Skip to content

🐛 Set X-Accel-Buffering: no on JSONL streaming responses#15813

Open
torrresagus wants to merge 2 commits into
fastapi:masterfrom
torrresagus:fix-jsonl-streaming-buffering-headers
Open

🐛 Set X-Accel-Buffering: no on JSONL streaming responses#15813
torrresagus wants to merge 2 commits into
fastapi:masterfrom
torrresagus:fix-jsonl-streaming-buffering-headers

Conversation

@torrresagus

@torrresagus torrresagus commented Jun 20, 2026

Copy link
Copy Markdown

What's wrong

FastAPI's Server-Sent Events responses set X-Accel-Buffering: no so streaming survives a buffering proxy (the is_sse_stream branch in fastapi/routing.py, added in #15030):

# For Nginx proxies to not buffer server sent events
response.headers["X-Accel-Buffering"] = "no"

The JSON Lines streaming responses (the yield-based application/jsonl streaming added in #15022, the is_json_stream branch) stream items incrementally in the same way, but don't set it.

Behind a proxy that buffers by default (e.g. Nginx with proxy_buffering on), the JSONL lines are buffered and flushed together, which defeats the streaming: the client receives the 200 immediately, but the lines don't arrive until the proxy buffer fills. SSE isn't affected because it already sends X-Accel-Buffering: no. The two features landed in separate PRs (SSE #15030, JSONL #15022), which is likely why only the SSE path got the header.

Change

Set X-Accel-Buffering: no on the JSONL StreamingResponse, so incremental streaming works through a buffering proxy the same way SSE does.

Cache-Control is intentionally not set: unlike SSE, JSON Lines is also used for bulk/streamed exports where caching can be legitimate, so the caching policy is left to the user (it can be set on the Response). This is documented in the JSON Lines tutorial. (Thanks @luzzodev for the review that clarified this.)

Tests

  • New test_stream_disables_proxy_buffering asserts x-accel-buffering: no across the four JSONL streaming tutorial variants (parametrized like the existing SSE streaming tests).
  • The existing SSE and streaming suites still pass.

Context

Discussed first in #15794, where the same failure mode was confirmed independently, including a report behind Nginx in production.

Server-Sent Events responses set `Cache-Control: no-cache` and
`X-Accel-Buffering: no` so proxies (e.g. Nginx) deliver events
incrementally instead of buffering the whole response.

JSONL streaming responses are incremental in the same way, but were
missing these headers, so a buffering proxy would hold back the lines
and defeat the streaming. Set the same headers on the JSONL response
for consistency with SSE.
@codspeed-hq

codspeed-hq Bot commented Jun 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing torrresagus:fix-jsonl-streaming-buffering-headers (ac21385) with master (d69774c)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (0cb4a8e) during the generation of this report, so d69774c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@luzzodev

Copy link
Copy Markdown
Contributor

Hello @torrresagus, thanks for the PR! I understand the point, but I'm not fully convinced yet.

JSONL isn't 1 to 1 "SSE but in JSON", it's also used for plain bulk/streamed exports where caching can be legitimate. So I'd split the two headers:

  • X-Accel-Buffering: no. No objection, buffering defeats streaming in every case.
  • Cache-Control: no-cache, On this one I'm hesitant about. It blocks the max-age "serve from cache without hitting origin" case that an export endpoint might want.

In any case the picked behaviour should be clearly pointed out in the docs.

@torrresagus

Copy link
Copy Markdown
Author

Thanks for the careful review @luzzodev — that's a fair distinction, and you've convinced me.

You're right that JSONL isn't semantically "SSE in JSON": it's also a transport for bulk/streamed exports where caching can be legitimate. And since Cache-Control: no-cache forces revalidation rather than meaning "don't store", defaulting to it would quietly take away the max-age "serve from cache without hitting origin" option from those export endpoints. That's a policy call the framework shouldn't make on the user's behalf — anyone who does want it can still set it explicitly via the Response.

X-Accel-Buffering: no is the one that's unambiguous: buffering defeats incremental streaming in every case, export or live.

So I'll drop Cache-Control and keep only X-Accel-Buffering: no, and add a short note in the JSONL streaming docs spelling out that the response disables proxy buffering (and that caching headers are left to the user). I'll update the test to match and push shortly.

…g, document behavior

Per review feedback: JSON Lines is not only a live event stream like SSE,
it's also used for bulk/streamed exports where caching can be legitimate.
`Cache-Control: no-cache` forces revalidation and would take away the
`max-age` "serve from cache" option from those endpoints, so it shouldn't
be imposed by default.

Keep only `X-Accel-Buffering: no` (buffering defeats incremental streaming
in every case) and leave caching headers to the user. Document the proxy
buffering behavior in the JSON Lines tutorial.
@github-actions

Copy link
Copy Markdown
Contributor

@luzzodev

Copy link
Copy Markdown
Contributor

LGTM!

@torrresagus

Copy link
Copy Markdown
Author

Thanks for the review @luzzodev!

@luzzodev luzzodev added the bug Something isn't working label Jun 23, 2026

@mohamedtaqysalmi mohamedtaqysalmi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mirroring the SSE anti-buffering behavior for JSONL is the right fix behind Nginx, and intentionally not imposing Cache-Control preserves legitimate export caching — the new docs section explains that well.

Small inconsistency: the PR title mentions both Cache-Control and X-Accel-Buffering, but
outing.py only sets X-Accel-Buffering. Either add the header for SSE parity or tighten the title/description so reviewers aren't looking for a header that isn't in the diff.

@torrresagus torrresagus changed the title 🐛 Set anti-buffering headers (Cache-Control, X-Accel-Buffering) on JSONL streaming responses 🐛 Set X-Accel-Buffering: no on JSONL streaming responses Jun 23, 2026
@torrresagus

Copy link
Copy Markdown
Author

Good catch @mohamedtaqysalmi — the title and description were stale after I dropped Cache-Control per the review. Re-adding it isn't the intent (it's deliberately left to the user), so I tightened the title and description to match the diff: only X-Accel-Buffering: no is set now. Thanks!

@torrresagus

Copy link
Copy Markdown
Author

@tiangolo Let me know if you need anything else from me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants