19 days ago
Two services in one project: same image, same database, byte-identical 110 KB front page.
Template: https://railway.com/deploy/rust-blog
Both are public and reproduce at will:
blog-test1.up.railway.app
blog-test2.up.railway.app
INTERLEAVED REQUESTS, SECONDS APART
blog-test1 ttfb=0.115 size=109889
blog-test2 ttfb=0.498 size=109889
blog-test1 ttfb=0.103 size=109889
blog-test2 ttfb=0.508 size=109889
blog-test1 ttfb=0.174 size=109889
blog-test2 ttfb=0.510 size=109889
Nine consecutive pairs, no overlap.
The app reports its own render time in a Server-Timing header, and the slow service is the one doing less work: blog-test2 builds the page in 24-39 ms, blog-test1 in 49-70 ms.
SIZE THRESHOLD
One warmed connection, so no DNS/TCP/TLS in the numbers:
146 B — 0.055 0.056 0.058 0.061 0.063
46,909 B — 0.055 0.056 0.059 0.061 0.064
83,208 B — 0.054 0.055 0.056 0.056 0.102
109,889 B — 0.475 0.481 0.481 0.522 0.532
The boundary sits between 83,208 and 109,889 bytes here; on an earlier stand with finer steps, between 82,291 and 88,524.
The penalty is flat — the same 250-430 ms at 88 KB as at 2.1 MB.
STATIC FILES ARE EXEMPT
nginx serves assets with try_files; / and /api/* go through proxy_pass. Same container, same connection, same curl invocation:
favicon.ico — 15,406 B — ttfb 0.032 — total 0.032
blog-ui-*.js — 28,921 B — ttfb 0.033 — total 0.034
blog-ui-*.wasm — 1,236,529 B — ttfb 0.032 — total 0.082
/ — 109,889 B — ttfb 0.478 — total 0.502
1.24 MB is delivered in full before the 110 KB page emits a first byte.
On penalised requests the body lands within 4-25 ms of the first byte. Nothing transfers slowly — the response is held, then released all at once.
RULED OUT
Payload size — flat penalty from 88 KB to 2.1 MB.
Bandwidth — 1.24 MB in 82 ms on the same connection.
Compression — with gzip the page is ~25 KB on the wire, still 297-526 ms.
nginx buffers — proxy_buffers 16 32k + proxy_max_temp_file_size 0, no change.
nginx listen — listen [::]:$PORT ipv6only=off, no change.
CPU — 4 vCPU, nr_throttled 0 during the stall.
Cloudflare — reproduces on the bare *.up.railway.app domain.
Data shape — 5, 13, 21, 26 rows; bodies 800 B to 200 KB.
From a shell inside the container, the same response is produced in under 100 ms whether or not the external request is penalised.
INTERMITTENT
blog-test1 was penalised on eight consecutive requests earlier today at a similar size, and is clean now — no redeploy, restart or config change in between. blog-test2 has been penalised throughout.
The state follows the container, holds for tens of minutes, and flips on its own.
QUESTION
- Is there a buffering layer for proxied responses above ~80-90 KB?
- Does anything determine whether a container lands in that mode?
- Is anything configurable to avoid it?
10 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
Independent confirmation from a completely different stack (Django + gunicorn, no nginx) — we spent today chasing what looks like the same edge behavior, from the streaming angle: https://station.railway.com/questions/edge-proxy-buffers-streaming-responses-4084b96c
Our findings, which complement yours:
- For Server-Sent Events (chunked, text/event-stream), the edge holds the ENTIRE response and releases it only at EOF — a diagnostic endpoint emitting 10 chunks at 500ms intervals arrives as a single burst after ~5s. That matches your "held, then released all at once" observation, but with no size threshold at all: even a 1MB leading payload is held until the stream closes.
- Immune to everything we threw at it: Cache-Control: no-cache, no-transform; X-Accel-Buffering: no; GET vs POST; compression on/off (identical with and without Accept-Encoding). Cloudflare ruled out (reproduces on the bare *.up.railway.app domain, same as you).
- From inside the container, localhost responds perfectly (progressive chunks every 50-100ms) — the hold is added between the container and the public edge. Matches your in-container finding.
So there seem to be (at least) two manifestations of one buffering layer in the proxied-response path: a flat ~250-430ms penalty for non-streaming bodies above ~85KB (your case), and hold-until-EOF for streaming responses of any size (ours).
One actionable idea for your intermittency ("state follows the container, flips on its own"): capture the response headers / request IDs on penalized vs clean episodes (x-railway-request-id or any edge identifier) and compare — if the penalized state correlates with which edge replica the container is routed through, that would explain why one service is clean and the identical one isn't, and give the Railway team a precise place to look.
19 days ago
We've isolated the issue as much as possible, and the evidence suggests the delay is occurring upstream of our application.
The application consistently generates responses in under 100 ms (confirmed both via Server-Timing and direct requests from inside the container). The additional 250–500 ms delay only appears for proxied dynamic responses above approximately 80–90 KB, while static files—even those over 1 MB—are delivered immediately.
The behavior is intermittent and appears to follow a specific container rather than the application or deployment. Since the penalty remains constant regardless of payload size or compression, it does not appear to be a bandwidth or application performance issue.
Could you confirm:
Is there any response buffering or optimization layer at the Railway edge for proxied HTTP responses?
Is the ~80–90 KB threshold expected behavior or a known issue?
Why would one container consistently exhibit this behavior while another identical container does not?
Is there any configuration or routing option that disables this behavior or forces immediate streaming of proxied responses?
19 days ago
Note: hitting "Redeploy" a few times usually helps — roughly 1 deployment in 5 comes out clean. So for now my workaround is to keep redeploying until I win ;D
https://railway.com/project/2061e25b-4a79-4f96-92ce-3d969c4768db
Some additional info:
INSIDE https://blog-test1.up.railway.app/
root@5367cc3fc971:/app# echo "PORT=$PORT"; for i in $(seq 1 10); do curl -s -o /dev/null -w "app ttfb=%{time_starttransfer} size=%{size_download}\n" "http://127.0.0.1:3000/"; curl -s -o /dev/null -w "nginx ttfb=%{time_starttransfer} size=%{size_download}\n" "http://127.0.0.1:$PORT/"; done
PORT=8080
app ttfb=0.058692 size=109889
nginx ttfb=0.052770 size=109889
app ttfb=0.050843 size=109889
nginx ttfb=0.052975 size=109889
app ttfb=0.054039 size=109889
nginx ttfb=0.053825 size=109889
app ttfb=0.051657 size=109889
nginx ttfb=0.048656 size=109889
app ttfb=0.052321 size=109889
nginx ttfb=0.053943 size=109889
app ttfb=0.053275 size=109889
nginx ttfb=0.056426 size=109889
app ttfb=0.064111 size=109889
nginx ttfb=0.049354 size=109889
app ttfb=0.050858 size=109889
nginx ttfb=0.055881 size=109889
app ttfb=0.048620 size=109889
nginx ttfb=0.050532 size=109889
app ttfb=0.052489 size=109889
nginx ttfb=0.053361 size=109889
OUTSIDE https://blog-test1.up.railway.app/
tikitko@MacBook-Pro-Nikita ~ % h=blog-test1.up.railway.app; for i in $(seq 1 10); do curl -s --http1.1 -w "ttfb=%{time_starttransfer} size=%{size_download}\n" -o /dev/null "https://$h/w-$RANDOM.js" -o /dev/null "https://$h/" | tail -1; done
ttfb=0.086309 size=109889
ttfb=0.133939 size=109889
ttfb=0.091684 size=109889
ttfb=0.084978 size=109889
ttfb=0.080868 size=109889
ttfb=0.080094 size=109889
ttfb=0.112140 size=109889
ttfb=0.081164 size=109889
ttfb=0.084543 size=109889
ttfb=0.080615 size=109889
INSIDE https://blog-test2.up.railway.app/
root@eb0e09ea0761:/app# echo "PORT=$PORT"; for i in $(seq 1 10); do curl -s -o /dev/null -w "app ttfb=%{time_starttransfer} size=%{size_download}\n" "http://127.0.0.1:3000/"; curl -s -o /dev/null -w "nginx ttfb=%{time_starttransfer} size=%{size_download}\n" "http://127.0.0.1:$PORT/"; done
PORT=8080
app ttfb=0.027741 size=109889
nginx ttfb=0.021462 size=109889
app ttfb=0.024064 size=109889
nginx ttfb=0.022877 size=109889
app ttfb=0.020958 size=109889
nginx ttfb=0.020556 size=109889
app ttfb=0.022519 size=109889
nginx ttfb=0.025755 size=109889
app ttfb=0.027148 size=109889
nginx ttfb=0.024757 size=109889
app ttfb=0.022692 size=109889
nginx ttfb=0.024048 size=109889
app ttfb=0.023715 size=109889
nginx ttfb=0.021974 size=109889
app ttfb=0.022836 size=109889
nginx ttfb=0.022434 size=109889
app ttfb=0.025861 size=109889
nginx ttfb=0.030469 size=109889
app ttfb=0.024980 size=109889
nginx ttfb=0.023474 size=109889
OUTSIDE https://blog-test2.up.railway.app/
tikitko@MacBook-Pro-Nikita ~ % h=blog-test2.up.railway.app; for i in $(seq 1 10); do curl -s --http1.1 -w "ttfb=%{time_starttransfer} size=%{size_download}\n" -o /dev/null "https://$h/w-$RANDOM.js" -o /dev/null "https://$h/" | tail -1; done
ttfb=0.578564 size=109889
ttfb=0.477331 size=109889
ttfb=0.542685 size=109889
ttfb=0.488595 size=109889
ttfb=0.479241 size=109889
ttfb=0.572960 size=109889
ttfb=0.504554 size=109889
ttfb=0.544505 size=109889
ttfb=0.483017 size=109889
ttfb=0.481072 size=109889
17 days ago
Any ideas here?
17 days ago
The issue is that Railway's edge router and your container's port are racing during startup, causing a flaky proxy tunnel about 4 out of 5 times.
To fix it, you need to make sure your app or Nginx is fully up and listening on the port before the container finishes starting, or add a slight startup delay.
Try wrapping your start command in a small script that verifies the port is active before letting the container exit or finish initializing, like this:
tikitko
Any ideas here?
17 days ago
can you send the current image or error displaying this was in my draft since yesterday
manuproject
The issue is that Railway's edge router and your container's port are racing during startup, causing a flaky proxy tunnel about 4 out of 5 times. To fix it, you need to make sure your app or Nginx is fully up and listening on the port before the container finishes starting, or add a slight startup delay. Try wrapping your start command in a small script that verifies the port is active before letting the container exit or finish initializing, like this:
17 days ago
thanks for the answer! I think you missed the example part in your comment. Also, then why do static files work fine?
tikitko
thanks for the answer! I think you missed the example part in your comment. Also, then why do static files work fine?
17 days ago
You're right, my original theory doesn't hold up against this — your local tests show the app and nginx are both fast internally regardless of whether the same request gets penalized externally, so it's not a startup race or anything in your container at all.
This looks like it's happening in Railway's edge/proxy layer, not your code. Two things stand out: the response is fully buffered then released all at once (not slow — held), and there's a hard size threshold around 85-90KB where it flips on. That pattern plus the fact that it follows the container and flips on its own over time points to something at the edge, maybe size-based routing or some kind of response inspection, not resource limits — you've already ruled those out.
16 days ago
Railway team, any ideas here?
