Caddy in front of an AI app: the stream arrives all at once, or dies at 60 seconds
Your chat backend streams when you call it directly, but through Caddy the answer lands in one burst, or the connection ends around the one-minute mark with no 5xx in either log. The detection chapter below is free and complete — run it before paying anything. Platform: Caddy 2.11.x (self-hosted reverse proxy). Change: 2.11.6, 2026-10-01, plus patches 2.11.7 (2026-10-03), 2.11.8 (2026-11-04) and 2.11.9 (2026-11-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
One patch release moved three things that a streaming AI app depends on, and the next three releases kept adjusting them. This is the window, read from the published release notes:
- 2.11.6 (2026-10-01) stopped
encodebuffering SSE. Release notes: “Server-sent events behindencodenow stream immediately instead of being buffered. (#7905)”. Before this, the encoder withheld the response header until the first body write, so an SSE handshake that sends only headers did not reach the client until the first event. - 2.11.6 added a 1-minute idle read/write timeout by default. Release notes: “New 1-minute idle read/write timeouts by default.” A request-body read or response write that makes no progress for that long aborts the connection. The same release added the
timeoutsdirective for per-route tuning. - 2.11.6 cut the request header limit from Go’s 1 MB default to 16 KiB. Release notes: “Request headers are limited to 16 KiB by default.” Anything larger gets
431 Request Header Fields Too Large; the new escape hatch is themax_header_sizeserver option. - 2.11.6 also stopped forwarding request header fields that contain a dot, the same way underscores were handled since 2.11.4. The new
expected_dot_headersserver option exists to allow the specific ones an app needs. - 2.11.7 (2026-10-03) fixed regressions that 2.11.6 introduced. Release notes: “a crash when proxying over HTTP/2 and streams that were cut off after a minute”. It also added support for the
Incremental: ?1header field (RFC 10036), described in the notes as “the standard replacement for NGINX’s proprietary X-Accel-Buffering header”. - 2.11.8 (2026-11-04) reworked idle timeouts again. Per-route timeouts now take precedence over the server-wide values, nested timeouts use the innermost one, and deadlines set by handlers act as a ceiling. 2.11.9 (2026-11-05) followed with regression fixes and security patches, mostly around FastCGI — it is not a streaming fix.
One correction to the advice you will find in blog posts and model answers: flush_interval -1
is not the fix. Caddy’s own reverse_proxy documentation says that option is ignored and
responses are flushed immediately when the response Content-Type is
text/event-stream, and the buffering in the pre-2.11.6 window happened one layer up, inside
encode. In issue #6293 the reporter tried flush_interval -1 and
flush_interval 1h and wrote “none of these settings change anything” (comment of 2024-12-29).
Detect: are you affected?
Three read-only checks. Run them in order; each one narrows what the next one can tell you.
Check 1 — the binary, and whether encode is in the route chain.
caddy version
caddy adapt --config /etc/caddy/Caddyfile --pretty 2>/dev/null \
| grep -n '"handler": "encode"'
| Output | Meaning | Next step |
|---|---|---|
| v2.11.5 or older | The encode-buffering window applies to you if encode wraps the stream route. | Check 2 |
| v2.11.6 | Buffering is fixed, but this release carries the idle-timeout regressions. | Check 3 |
| v2.11.7 / 2.11.8 / 2.11.9 | No encode-buffering or known stream-cut regression at this patch level; the default 1-minute idle timeouts still exist. | Check 3 |
no "handler": "encode" line | The encoder is not in this route chain, so the pre-2.11.6 buffering cannot be the cause. | Stop if check 2 also streams; otherwise check the upstream and other proxies |
Check 2 — first byte versus total time, through Caddy and straight to the upstream. Run each command three times: model latency varies, buffering does not.
curl -sN -o /dev/null -w 'via-caddy ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
-H 'Accept: text/event-stream' https://ai.internal/api/chat
curl -sN -o /dev/null -w 'direct ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
-H 'Accept: text/event-stream' http://127.0.0.1:3000/api/chat
| Output | Meaning | Next step |
|---|---|---|
| via-caddy ttfb≈total, direct ttfb << total | The stream is being held by a layer Caddy adds. On 2.11.5 and older, that is encode. | Failure mode 01 below; paid chapter has the route split |
| both ttfb ≈ total | The upstream itself is not streaming. This runbook will not fix it. | Stop; fix the app’s SSE output |
| both ttfb << total | Bytes arrive while the answer is generated; buffering is not your problem. | Check 3 |
| curl: (52) / 502 | The request did not reach a working upstream. | Stop; this is a routing problem, not this runbook |
Check 3 — where a long stream ends. Give it two minutes and watch the total time and the tail of the body.
curl -sN -o /tmp/sse.out --max-time 120 \
-w 'http=%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
-H 'Accept: text/event-stream' https://ai.internal/api/chat
wc -c /tmp/sse.out
tail -c 200 /tmp/sse.out
| Output | Meaning | Next step |
|---|---|---|
| total ≈ 60s, body ends mid-event | A one-minute idle timeout (2.11.6 and later) or the 2.11.6 stream-cut regression is the likely cause. | Failure mode 02 below |
| total ≈ 120s and still streaming | No idle cut at the one-minute mark in this path. | Stop if checks 1–3 show nothing |
| total < 5s, complete answer | The app answered without streaming at all. | Stop; not this runbook |
| HTTP/2 stream error in the Caddy log | Matches the 2.11.6 quiet-stream reset that 2.11.7 fixed. | Failure mode 02; upgrade to 2.11.7 or later |
If check 1 shows no
encode handler on the stream route, check 2 streams through Caddy as well as it
does directly, and check 3 runs past two minutes, this runbook is not your problem and you should not buy it.
Email [email protected] with the three outputs
and your caddy version if you want a second opinion first.
Failure modes
Seven distinct failure shapes, each tied to a version line or a default that changed. Two are visible here.
On Caddy 2.11.5 and older, with
encode in the chain (its default matcher includes
text/*, so it swallows text/event-stream), the encoder holds the response header until
the first body write. The SSE handshake only completes on the first event, and each later event can sit in a
compression frame. curl -N through Caddy shows ttfb close to
total; the same request straight to the upstream streams. The fix direction is to get the stream
out of encode’s path — upgrade, or route the SSE prefix without the encoder.
2.11.6 introduced default 1-minute idle read/write timeouts. Its own patch notes a week later admit two failure shapes: over HTTP/1.1, streams opened with a POST “were cut off exactly 60 seconds after the body was read”; over HTTP/2, quiet streams that paused between writes “were reset with a stream error”. Both were fixed in 2.11.7. On later patches the timeout still defaults to one minute, so a stream that stops making progress can still be cut. Whether a GET stream that sends nothing at all for more than 60 seconds before its first token is cut is not documented and was not measured for this edition — treat it as inferred and test it with check 3.
Failure modes 03–07: the copied
flush_interval -1 fix that changes nothing; the 16 KiB
request-header cap that answers 431; per-route timeouts that silently do
nothing before 2.11.8; request headers containing a dot that disappear before your auth middleware sees them; and the
2.11.7–2.11.9 patch drift, including what 2.11.8 changed about encode and
Cache-Control: no-transform. The migration checklist is split by component (binary, encoder,
streaming route, timeouts, upstream app, auth headers, monitoring), with a rollback plan and a 24-hour 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 application usually keeps streaming; what changes is which layer is allowed to touch the bytes and how long each layer waits. The runbook splits the work by component:
| Component | What breaks | What the runbook has you change |
|---|---|---|
| Caddy binary | 2.11.6 fixed buffering and introduced the 60-second cut; 2.11.9 has the current fixes | Pin a specific patch, with 2.11.7 as the minimum and 2.11.9 as the target |
encode handler | Pre-2.11.6: headers and events held in the encoder | Upgrade, or keep the SSE prefix out of every encoder route |
| Streaming route | Response type, response buffers, compression settings | Content-Type: text/event-stream; no response buffering; optional Incremental: ?1 from the app on 2.11.7+ |
| Timeout configuration | 1-minute idle default and per-route precedence differences | Server-wide read_body_idle / write_idle; per-route only on 2.11.8+ |
| Auth headers and cookies | 16 KiB header cap and dot/underscore header filtering | max_header_size, expected_dot_headers, expected_underscore_headers, smaller session cookies |
| Monitoring | Streams die with no 5xx in the app log | Alert on 431 counts, 499s, HTTP/2 stream resets and the write-timeout log line |
Appendix
- Caddy v2.11.6 release notes, published 2026-10-01 (SSE behind encode, 1-minute idle timeouts, 16 KiB headers, dot headers)
- Caddy v2.11.7 release notes, published 2026-10-03 (fixes the 2.11.6 crash and 60-second stream cuts; adds
Incremental: ?1) - Caddy v2.11.8 release notes, published 2026-11-04 (idle timeouts, round two: per-route precedence)
- Caddy v2.11.9 release notes, published 2026-11-05 (regression and security fixes; mostly FastCGI)
- caddyserver/caddy #7905 — “encode: flush headers immediately for server-sent events responses”, merged 2026-07-31 (the fix that shipped in 2.11.6)
- caddyserver/caddy #6293 — “Problems with reverse proxied server sent events and compression”, opened 2024-05-02; the 2024-12-29 comment records that
flush_interval -1changed nothing - Caddy global options —
timeoutsdefaults (1m idle) andmax_header_sizedefault 16KiB (reviewed 2026-11-08) - Caddy
timeoutsdirective — per-route timeouts and the pre-2.11.8 precedence caveat (reviewed 2026-11-08)
The detection chapter above is reviewed against the release notes, the pull request and the issue linked in this appendix, and is complete. The paid chapter (failure modes 03–07 and the migration checklist) is still in re-verification on a live Caddy 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].
One block instead of a stream, or a cut at 60 seconds?
Send the three detection outputs and your caddy version. If it is this window, the migration is a handful of Caddyfile lines, not a rewrite.