Runbook 04 · Cloudflare edge

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 JS and CSS. An unhashed /assets/app.js can 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 BYPASS when the origin answer is not cacheable, and HIT when it is. The rule’s Edge TTL mode decides whether origin Cache-Control is 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 for br or gzip on every request, and Compression Rules can target the stream path anyway. Cache-Control: no-transform is 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 tells cloudflared to 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):"
OutputMeaningNext step
cf-cache-status: DYNAMICNot eligible for cache. Normal for a stream. The edge is not the cache problem; continue to check 2.Check 2
cf-cache-status: BYPASSSomething 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: HITThis 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/jsonA 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 / brThe 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'
OutputMeaningNext step
first_byte=0.4s total=9.8sBytes arrive while the answer is still being generated. Streaming works; look elsewhere.Check 3 for rewrites
first_byte=9.7s total=9.8sNothing reached the client until the response ended. This is buffering, whatever the UI showed.Failure mode 01
first_byte≈total on every runConsistent across requests, so it is a configuration layer, not model latency.Failure mode 01 / 05
Connection closes at a fixed long intervalA 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}'
OutputMeaningNext 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 *.htmlA cache rule reaches beyond static assets.Failure mode 02 or 04
"result": [] or no rulesNo 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.

01 — The stream that arrives as one payload
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.
02 — The cache rule that answered with someone else’s answer
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.
The remaining three, and the migration, are in the paid bundle
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:

ComponentWhat breaksWhat the runbook has you change
Streaming endpointBuffered until close; one blob at the endOrigin sends text/event-stream; bypass cache on the stream path; no compression rule matches it
Dynamic JSON APICross-user answers; HIT and Age on an authenticated routeBypass cache for the API prefix, respect origin TTL, purge, keep no-store authoritative
Front-end bundleDeferred or rewritten scripts; CSP violationsRocket Loader off, Auto Minify off, data-cfasync="false" only where the setting must stay
HTML and app shellStale HTML pointing at an old bundle after deployEither bypass HTML or keep its Edge TTL short; hashed asset filenames
Tunnel / reverse proxyCompressed chunks, X-Accel-Buffering, read timeoutsOne 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

Verification status
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.
For agents

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.

Request one
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.