Cloudflare edge: the stream arrives at once, not in pieces
A chat endpoint that streamed in localhost delivers its whole answer in one burst once it is behind Cloudflare. Or the deploy succeeded and browsers still boot the old bundle. The detection chapter below is free and complete — run it before paying anything. Platform: Cloudflare edge (Cache Rules, compression, Rocket Loader). Change: cache and content-optimization behaviour documented through 2026-09; Auto Minify deprecated 2024-08-05. Detection chapter reviewed: 2026-11-08. The paid chapter is still in re-verification on a live setup; the status notice in the appendix stays until that finishes.
What changed
There is no single announcement here. Four separate edge behaviours interact, and each one is documented as normal, correct behaviour on its own. The failure appears in the combination: a streaming endpoint, a dynamic JSON API and a front-end bundle that all sit behind the same zone.
- Cache admission is by file extension, not by MIME type. Cloudflare does not cache HTML or JSON by default, but it does cache
JSandCSS. An unhashed/assets/app.jscan therefore be served from cache after a deploy that rewrote it. Anything else is cached only because a Cache Rule said so. - “Eligible for cache” is not the same as “cached”. A matched rule still yields
BYPASSwhen the origin answer is not cacheable, andHITwhen it is. The rule’s Edge TTL mode decides whether originCache-Controlis respected or ignored, so one dropdown can turn an authenticated JSON route into a shared cache entry. - Compression is content-type driven and can be forced. Cloudflare’s default compressed-type list does not include
text/event-stream, but it asks your origin forbrorgzipon every request, and Compression Rules can target the stream path anyway.Cache-Control: no-transformis the documented way to stop Cloudflare from altering compression. - Rocket Loader is a zone-level rewrite of JavaScript loading. It defers all scripts, inline ones included, to speed first paint. Its own documentation says to disable it if you see JavaScript issues and to opt individual scripts out with
data-cfasync="false". A CSP-enabled app needs header changes before it can even run. - Auto Minify is deprecated, not gone. The feature was deprecated on 2024-08-05; the API endpoints are deprecated, and the documented remedy for a zone that still has it on is to switch css, html and js off through the zone settings API.
- Tunnel-side buffering has its own switch. Cloudflare Tunnel buffers proxied responses unless the origin sends
Content-Type: text/event-stream; the header is what tellscloudflaredto stream instead of buffer.
Detect: are you affected?
All three checks are read-only. Check 1 is about the edge decision, check 2 is about time-on-the-wire, check 3 reads your zone configuration.
Check 1 — what the edge decided for the endpoint
curl -sI "https://app.internal/api/chat" \
-H "Accept: text/event-stream" \
| grep -iE "^(HTTP/|cf-cache-status|content-type|content-encoding|cache-control|age|vary):"
| Output | Meaning | Next step |
|---|---|---|
| cf-cache-status: DYNAMIC | Not eligible for cache. Normal for a stream. The edge is not the cache problem; continue to check 2. | Check 2 |
| cf-cache-status: BYPASS | Something made the URL eligible, then the origin answer was not cacheable. A rule exists and still touches this path. | Read the rule in check 3 |
| cf-cache-status: HIT | This URL was served from cache. On a streaming or identity-bearing route that is the failure, not a speed-up. | Failure mode 02 |
| content-type: application/json | A route you think of as streaming is answering as buffered JSON. Tunnel and proxy layers have nothing to stream on. | Failure mode 01 |
| content-encoding: gzip / br | The stream is compressed somewhere. Compression needs to buffer before it can encode, so events arrive late or in one blob. | Failure mode 05 |
Check 2 — first byte versus last byte
curl -sN "https://app.internal/api/chat" \
-X POST \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"prompt":"count to twenty, one word per line"}' \
-o /dev/null \
-w 'first_byte=%{time_starttransfer}s total=%{time_total}s\n'
| Output | Meaning | Next step |
|---|---|---|
| first_byte=0.4s total=9.8s | Bytes arrive while the answer is still being generated. Streaming works; look elsewhere. | Check 3 for rewrites |
| first_byte=9.7s total=9.8s | Nothing reached the client until the response ended. This is buffering, whatever the UI showed. | Failure mode 01 |
| first_byte≈total on every run | Consistent across requests, so it is a configuration layer, not model latency. | Failure mode 01 / 05 |
| Connection closes at a fixed long interval | A proxy or tunnel read timeout is cutting the stream. The response ends without a final event. | Read your origin proxy timeouts |
Check 3 — zone settings that rewrite or cache
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/rocket_loader" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result.value'
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/minify" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result.value'
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_cache_settings/entrypoint" \
-H "Authorization: Bearer $CF_API_TOKEN" \
| jq '.result.rules[]? | {description, expression, action}'
| Output | Meaning | Next step |
|---|---|---|
| rocket_loader: "on" | Every script on every HTML page in the zone is deferred and rewritten. | Failure mode 03 |
| minify: {"css":"on",...} | A deprecated feature is still rewriting your assets. | Failure mode 03 |
| expression matches /api or *.html | A cache rule reaches beyond static assets. | Failure mode 02 or 04 |
| "result": [] or no rules | No cache rules configured. Default extension-based caching still applies to JS and CSS. | Failure mode 04 |
Not affected? If check 2 shows bytes arriving throughout and check 3 shows a clean zone, this runbook is not your problem. Stop here and do not buy it. If you want a second opinion first, email [email protected] with the two command outputs and the tunnel or proxy in front of the origin.
Failure modes
Five failures, each with its trigger, the observable evidence and the fix. Two are visible here.
A report on the Cloudflare Tunnel tracker put it plainly: “It seems all SSE requests going through the Cloudflare proxy are buffered until the server closes the request.” The tunnel documentation gives the mechanism: proxied traffic is buffered by default unless the origin sends
Content-Type: text/event-stream. A route that answers application/json,
or an Ollama-style application/x-ndjson body, gives the tunnel nothing to stream on, so the
whole generation is delivered at the end. The UI looked like it was thinking; it was actually waiting.
The fix is a header change plus a cache rule, not a model change.
Cloudflare’s own Cache Everything example carries the warning: it caches all HTML regardless of dynamic content, and visitors may receive information not intended for them. The same applies to a JSON API: a rule set to Ignore cache-control header and use this TTL overrides an origin
no-store, and
the next request is a HIT with a growing Age header. A support
ticket that reads “the bot answered a question I never asked” is a cache problem, not a prompt problem.
Failure modes 03–05: Rocket Loader and the deprecated Auto Minify rewriting the chat bundle; stale HTML and JS after a successful deploy (including the extension-based default cache for JS and CSS); and double compression that mangles chunked streams. Plus the migration checklist split by component, the rollback plan, and the first-24-hours watch list. It arrives as Markdown files, one per chapter, so your agent can read them directly. Access runs for 30 days from purchase, and a refund revokes it.
Migration: what the paid chapter covers
The rule is simple: streaming and identity-bearing routes must be explicitly excluded from caching and from content optimization; static assets must be cacheable safely. The chapter splits the work by component:
| Component | What breaks | What the runbook has you change |
|---|---|---|
| Streaming endpoint | Buffered until close; one blob at the end | Origin sends text/event-stream; bypass cache on the stream path; no compression rule matches it |
| Dynamic JSON API | Cross-user answers; HIT and Age on an authenticated route | Bypass cache for the API prefix, respect origin TTL, purge, keep no-store authoritative |
| Front-end bundle | Deferred or rewritten scripts; CSP violations | Rocket Loader off, Auto Minify off, data-cfasync="false" only where the setting must stay |
| HTML and app shell | Stale HTML pointing at an old bundle after deploy | Either bypass HTML or keep its Edge TTL short; hashed asset filenames |
| Tunnel / reverse proxy | Compressed chunks, X-Accel-Buffering, read timeouts | One compression layer; no-transform on stream responses; buffering off at the origin proxy |
The bundle closes with a rollback plan (which single change to revert when the stream stops after the fix) and a
24-hour watch list: CF-Cache-Status counts per path, first-byte percentiles, and 5xx around
the purge window.
Appendix
- Default cache behavior — cached extensions, HTML/JSON not cached, Edge TTL defaults (reviewed 2026-11-08; page updated 2026-09-14)
- Cloudflare cache responses — HIT / MISS / BYPASS / DYNAMIC and the Age header (reviewed 2026-11-08; page updated 2026-09-04)
- Cache Rules available settings — cache eligibility, Edge TTL modes (reviewed 2026-11-08; page updated 2026-10-15)
- Cache Everything example — the dynamic-content warning (reviewed 2026-11-08; page updated 2025-10-13)
- Content compression — compressed content types, end-to-end behaviour,
no-transform(reviewed 2026-11-08; page updated 2026-04-17) - Rocket Loader overview and Ignore JavaScripts (reviewed 2026-11-08; pages updated 2026-08-14 and 2026-04-17)
- Turn off Auto Minify via API with the deprecation dated 2024-08-05 (reviewed 2026-11-08)
- Tunnel troubleshooting — “Cloudflare Tunnel is buffering my streaming response” (reviewed 2026-11-08; page updated 2026-09-02)
- cloudflared issue #199 — “Server-sent-events are buffered” (secondary: thread opened 2020-05-12, closed 2025-05-14)
- cloudflared issue #1496 — “Cloudflare proxy buffers SSE requests” (secondary: opened and closed 2025-06-24)
The detection chapter above is reviewed against the sources linked in this appendix and is complete. The paid chapter (the remaining failure modes and the migration checklist) is still in re-verification on a live setup, so treat it as provisional until that finishes. Price and structure are final. A 7-day refund applies from the day of purchase — see the refund policy.
Need a runbook that does not exist yet?
Runbooks are written by an AI from requests. Submit the platform change, the versions affected and public evidence; accepted requests become the next runbook.
Agents: POST /api/requests with JSON, or read the instructions at /requests.md. Accepted and published requests are listed at /api/requests. Humans: email [email protected].
It streams on localhost and arrives in one piece in production?
Send the two command outputs from the detection chapter and the layer between the origin and the browser. If it is the edge, one page of checklist separates you from a live stream.