Edge proxy holding specific POST requests ~20s without forwarding to upstream (499)
dgiannetto-dev
HOBBYOP

25 days ago

We run a Fastify API (project: thermocline/api, production) behind Railway's edge. From one iOS client (React Native/Expo, NSURLSession), a specific POST /v1/conditions request reliably stalls on every cold app open: your HTTP Logs show the request arriving at the edge and sitting 19–20 seconds until our client timeout aborts it (logged 499) — while our application-level instrumentation (Fastify onRequest/onRequestAbort hooks) confirms these requests are never forwarded to the upstream container at all (zero corresponding app-side records).

Key facts:

Sibling requests from the same device in the same second — GETs and other POSTs with auth headers and JSON bodies — are forwarded and served in <500ms

Reproduced on Wi-Fi and cellular (different networks, same result), across many app builds

Body size ruled out: reproduced with both ~1.2KB and ~650B bodies

Concurrency ruled out: reproduced after reducing the app's cold-open burst from ~16 to ~8 simultaneous requests

The same request shape sent from a desktop HTTP client completes 40/40 at ~40ms median

Occasionally one of these held requests completes with 200 after ~18s at the edge (upstream serve time ~2s), suggesting the request is queued/held edge-side before forwarding

Question: under what conditions does the edge accept a request (visible in HTTP Logs) but delay/never forward it upstream? Is there per-stream flow-control, buffering, or HTTP/2 handling on the edge that could hold an iOS-originated POST body for ~20s? Happy to provide timestamps, request IDs, and our logs.

$10 Bounty

2 Replies

Railway
BOT

25 days ago

This thread has been opened as a bounty so the community can help solve it.

Status changed to Open Railway 25 days ago


devsaswave
HOBBY

20 days ago

Question 1:

The most common reason is a communication failure between Railway's Edge Proxy and your application. When this happens, the request might be logged at the edge, but it will fail to be forwarded

Key causes include:

  • Application under heavy load, it may not be able to accept new connections. You can check the Metrics tab in your Railway service panel to see if your CPU or memory usage is maxed out .
  • Incorrect host or port binding, your application must listen on the host 0.0.0.0 and use the PORT environment variable provided by Railway. If it's bound to localhost or a hardcoded port, the edge proxy will be unable to connect, resulting in a "connection refused" error .

Question 2:

Yes, the combination of HTTP/2 flow-control and NSURLSession's internal scheduling can definitively introduce delays of 20 seconds or more for iOS-originated POST requests.

NSURLSession Sends Request Bodies Sequentially: By design, when an iOS app uses NSURLSession to make multiple concurrent HTTP/2 upload requests, the request headers are sent immediately, but the request bodies (the DATA frames in HTTP/2) are sent one at a time, not interleaved across streams. This means that for a specific POST request, its body can be held, waiting while the connection is busy sending the body of another request.

The 64KB Default Window: HTTP/2 also has flow control to prevent one stream from using all the resources. The initial "window" for sending data on a new stream is 65,535 bytes (64KB). The server sends WINDOW_UPDATE frames to grant more credits as it processes data.

A Race Condition Can Stall a Stream: A critical nuance of HTTP/2 flow control is a potential race condition. If the client sends a full 64KB window of data before it receives the server's SETTINGS frame (which may adjust this window), the client's window can become negative, stalling the stream until the server sends a WINDOW_UPDATE to make it positive again.

While the client's HTTP/2 behavior is likely the primary cause, Railway's edge network can also contribute to or amplify delays. They have documented incidents of elevated latency affecting edge locations, including US West, Europe, and Asia. These platform-level issues can add to the time your request is waiting, easily pushing it past a 20-second mark if it's already being queued on the client.


marlonwq
FREE

19 days ago

Hey! Great debugging so far. Since you've already ruled out payload size and concurrency, this smells exactly like the classic iOS Expect: 100-continue deadlock.

By default, NSURLSession (which React Native uses under the hood) injects an Expect: 100-continue header on POST requests. It sends the headers first and waits for the server to reply with a '100 Continue' before actually sending the data body.

The issue is that reverse proxies (like Railway's edge) often wait to receive the full request body before opening a connection to your upstream app. So, iOS is waiting for the proxy, the proxy is waiting for iOS, and nobody does anything until your 20s client timeout kills the connection (hence the 499). Fastify never even sees it.

As a quick test, try explicitly overriding or stripping the Expect header in your client's request setup (just set it to an empty string) to force iOS to send the payload immediately. That usually bypasses this exact stall.


Welcome!

Sign in to your Railway account to join the conversation.

Loading...