19 days ago
Our Django/gunicorn service streams Server-Sent Events correctly when tested against gunicorn directly (localhost inside the container: chunks arrive every 50-100ms). But through the public edge, the entire response is held and delivered in a single burst when the stream ends.
One-command repro (diagnostic endpoint, emits 10 SSE chunks at 500ms intervals):
curl -sN https://web-production-6f298.up.railway.app/healthz/stream/Expected: one "data:" line every ~500ms. Actual: all 10 arrive together after ~5s.
Same result through our Cloudflare-proxied custom domain (api.medicallearninglab.com), so the CDN is ruled out — the buffering happens at the Railway edge.
Already ruled out on our side: Content-Type: text/event-stream, Cache-Control: no-cache, no-transform and X-Accel-Buffering: no are all set; identical behavior with GET and POST, with and without compression (Accept-Encoding), and even with a 1MB leading payload (so it's not a flush-threshold buffer). gunicorn 25.1.0, gthread workers, chunked responses.
Service: web (d0e2aa40-41a8-4b0c-9664-dfa35360a061), environment: production (71595b01-c5e8-4101-93cd-d730ddd90918).
Is response buffering configurable for this service, or is this a known edge issue?
8 Replies
19 days ago
This thread has been opened as a bounty so the community can help solve it.
Status changed to Open Railway • 19 days ago
19 days ago
Based on our testing, it appears that response buffering is occurring at the Railway edge. Since the application streams correctly when accessed directly via Gunicorn on localhost, and the same buffering occurs on both the Railway public URL and our Cloudflare-proxied custom domain, we believe the issue is outside the application itself.
19 days ago
I tried to deploy a simple SSE myself and it works as expected, You can try run curl on this URL https://gzip-stream-production.up.railway.app
Do you have CDN caching enabled on your service settings? Is your repro code public?
ahmed12dev
Based on our testing, it appears that response buffering is occurring at the Railway edge. Since the application streams correctly when accessed directly via Gunicorn on localhost, and the same buffering occurs on both the Railway public URL and our Cloudflare-proxied custom domain, we believe the issue is outside the application itself.
19 days ago
Can you share your test repo? I will try deploy yours too
19 days ago
Thanks for looking! Answers to both questions plus one finding that might be the key:
CDN caching: not enabled. Verified via the API — our service's edge config reads:
edgeConfig { enabled: true, overrides: {}, caching: null }Note the enabled: true: we never turned anything on, and updateServiceEdgeConfig only exposes caching, so this flag isn't user-settable. Is edgeConfig.enabled a platform-side rollout of a newer edge/proxy path? That would explain the difference with your demo: your gzip-stream demo streams perfectly from our network (one chunk per second, measured just now), while our endpoint — same minute, same network — still delivers everything in one burst. So the edge CAN stream; something specific to how our service is fronted is buffering.
Repro code: yes, public in essence — it's this Django view (no auth, no DB), live at https://api.medicallearninglab.com/healthz/stream/ :
def healthz_stream(request):
def gen():
for i in range(10):
yield f"data: chunk {i} t={time.time():.2f}\n\n"
time.sleep(0.5)
resp = StreamingHttpResponse(gen(), content_type="text/event-stream")
resp["Cache-Control"] = "no-cache, no-transform"
resp["X-Accel-Buffering"] = "no"
return resp
# gunicorn 25.1.0, --workers 3 --threads 8 --worker-class gthreadExpected: one line every 500ms. Actual: all 10 after ~5s. From inside the container against localhost it streams correctly, so the hold is added between the container and the public edge.
Possibly related: the open bounty about a 110KB page being "held, then released all at once" with a flat ~400ms penalty (https://station.railway.com/questions/two-identical-services-one-110-kb-page-839d1d34) describes the same hold-and-release signature, intermittent per container — consistent with an edge-path rollout affecting some services/containers and not others.
mayori
Can you share your test repo? I will try deploy yours too
19 days ago
Here you go: https://github.com/devmedical/sse-buffering-repro
Minimal single-file Django app, same gunicorn version and flags as our affected service (25.1.0, --workers 3 --threads 8 --worker-class gthread; start command included in railway.toml). /healthz/stream/ emits 10 SSE chunks at 500ms intervals — locally it streams chunk-by-chunk (verified before pushing).
If it streams fine when YOU deploy it, that supports the idea that this is per-service/per-account edge routing rather than the app: our production service shows edgeConfig { enabled: true, caching: null } via the API (we never set it, and updateServiceEdgeConfig only exposes caching). Happy to deploy this same repro as a fresh service on our own account too, if it helps isolate whether the buffering follows the account/rollout or the specific service.
19 days ago
Can't reproduce it, I deployed your repo and everything works as expected
curl -sN https://sse-buffering-repro-production.up.railway.app/healthz/stream/
Happy to deploy this same repro as a fresh service on our own account too, if it helps isolate whether the buffering follows the account/rollout or the specific service
Yeah, try deploy it on your workspace, make the project public and share the project link here
18 days ago
Done — and the result is decisive:
Public project: https://railway.com/project/8470818b-4452-430e-906f-8632701f9136
Live: https://sse-buffering-repro-production-9f19.up.railway.app/healthz/stream/
The fresh service on the SAME account/workspace streams perfectly (exact 500ms cadence), while the original web service — measured minutes apart, and re-checked again just now — still delivers everything in one burst. Same account, same code (the repro is a copy of the affected view), same gunicorn version and flags.
Two more data points:
- The affected service has been redeployed 10+ times today (unrelated work) and the buffering persisted through every deploy — the state sticks to the service (or its domains/routing), not to the container. Slightly different from the 110KB-bounty case, where it flips on its own.
- Correcting my earlier hypothesis: both services report identical
edgeConfig { enabled: true, overrides: {}, caching: null }via the API, so that flag is NOT the discriminator. Whatever differs between these two services is internal to how the affected one is fronted/routed.
Affected service again for reference: web (d0e2aa40-41a8-4b0c-9664-dfa35360a061), env production (71595b01-c5e8-4101-93cd-d730ddd90918). Happy to run any diagnostic against it.
5 days ago
Update with new hard data — buffering is hold-until-EOF and size-independent (rules out any flush threshold)
Following up on the earlier finding (a fresh service in the same account streams fine; this service keeps buffering across redeploys): we instrumented the affected service to test the "byte threshold" hypothesis, and it's now conclusively dead.
Experiment: we prepended a configurable SSE comment preamble (:ppp…\n\n, emitted immediately, before any generation delay) and measured arrival timing from an external client (raw HTTP client, Accept-Encoding: identity, so no compression anywhere in the path). The server emits the stream over ~2–3 s in small chunks with 30–120 ms gaps (verified locally). Results through the public edge:
| Preamble size | Total bytes | First byte arrives | Delivery |
|---|---|---|---|
| 0 (none) | 1.5 KB | t = 2.38 s (= EOF) | single burst |
| 4 KB | 5.6 KB | t = 2.54 s (= EOF) | single burst |
| 16 KB | 17.9 KB | t = 2.63 s (= EOF) | single burst |
| 64 KB | 67 KB | t = 2.82 s (= EOF) | single burst |
Even 64 KB emitted instantly at stream start does not produce an early first byte: the first byte always coincides with stream completion. The edge is holding the entire response until EOF, regardless of size — so there is no buffer threshold to push past.
Response headers on the affected path (all correct for SSE):
Content-Type: text/event-stream
Transfer-Encoding: chunked
Cache-Control: no-cache, no-transform
cf-cache-status: DYNAMIC
x-railway-edge: mia1
CF-RAY: a2bde1629df9b449-SCLNotes: this measurement went through our Cloudflare-proxied custom domain, but the client requested identity encoding and CF reports DYNAMIC (no caching/compression), and the original Aug-2 diagnosis showed the same burst behavior. Combined with the fact that a brand-new service in the same account/project streams correctly with identical edgeConfig, everything points to per-service state on the edge routing layer (mia1 observed).
Service: project insightful-courage / environment production / service web (region us-west2, domains api.medicallearninglab.com + lite.medicallearninglab.com). Measurements taken 2026-08-16 ~04:45–05:05 UTC.
Could you check the edge routing/config state for this specific service? Happy to run any diagnostic you need on our side. If recreating the service is the recommended workaround, please confirm — we'd need to migrate env vars and re-attach both custom domains, so we'd rather exhaust the routing check first.