Nginx Connection Timeout Troubleshooting: How Four Timeout Parameters Divide the Work, with Live Tests
The moment an application behind Nginx starts responding slowly, timeout problems follow: the user sees a 504 after a spinning page, and the logs show a line of upstream timed out. But Nginx has more than a dozen directives with “timeout” in the name — without a clear picture of which parameter governs which segment, tuning is a coin toss. This post places keepalive_timeout and the three proxy_* timeout parameters into the complete path of a single request, provides a baseline configuration for long-running applications, and uses a live container test to show the full scene of a read timeout triggering a 504.
Test Environment
Conclusions should be reproducible, so here are the conditions first:
- nginx/1.31.5 (docker image
nginx:alpine, a throwaway container, deleted after use); - inside the same container, a silent backend that “accepts requests but never responds” started with
nc, simulating a slow application; - after each configuration change,
nginx -s reload, withcurlverifying status codes and elapsed time.
Four Parameters, Four Segments
The core mechanism in one sentence: a proxied request crosses two segments — “client↔Nginx↔backend” — and each timeout parameter guards exactly one; segment the path first, then tune.
| Parameter | Segment guarded | What happens when it fires |
|---|---|---|
keepalive_timeout |
Client ↔ Nginx, idle-connection keep-alive duration | Nginx closes the idle connection; the client rebuilds on its next request |
proxy_connect_timeout |
Nginx → backend, TCP connection setup | 504, log shows while connecting to upstream |
proxy_read_timeout |
Nginx ← backend, waiting for response data | 504, log shows while reading response header |
proxy_send_timeout |
Nginx → backend, sending the request body | 504, log shows while sending request to upstream |
Two most-misunderstood points, cleared up first:
proxy_read_timeoutdoes not mean “the entire response must complete within N seconds” — it means “no more than N seconds between two consecutive reads of data.” As long as the backend keeps emitting data, a ten-minute long response never triggers it. Conversely, if the backend goes silent for 3 seconds and then delivers the result, even a 2-second read timeout will kill it. Whether a slow endpoint needs this value raised depends on whether the backend is “slow but moving” or “silent first, result last.”keepalive_timeoutgoverns client-side connection reuse and has nothing to do with the backend. Raising it when the backend is slow is tuning the wrong knob.proxy_send_timeoutis the most overlooked one: it governs Nginx sending the request body to the backend — the maximum gap “between two consecutive writes.” It only comes into play when a client uploads large files and the backend receives slowly; ordinary API traffic is too small to ever trigger it.
Example 1: A Baseline Configuration for Long-Running Applications
For an application with long backend interaction times, write the timeout-related parts of the Nginx config like this:
1 | server { |
Rationale for the values: proxy_connect_timeout at 30 seconds is already a generous ceiling — a failed connection basically means the backend is down or the network is broken, and waiting longer is pointless; in practice many teams compress it to 5-10 seconds for fast failure. proxy_read_timeout at 300 seconds leaves ample room for long-running requests that “process silently first and return everything at the end.” keepalive_timeout at 300 seconds lets browsers reuse connections and skip repeated three-way handshakes — a clear win for pages rich in static assets.
After writing the config, let Nginx check itself first:
1 | nginx -t |
The live output was syntax is ok / test is successful, meaning the config is clean at the syntax level. After nginx -s reload takes effect, move on to the live test in the next section.
Example 2: The Full Scene of a Read Timeout Triggering a 504
To watch proxy_read_timeout behave, swap the backend for a silent service that “takes the request and says nothing” (nc listening on port 9001, accepting the request but never sending a byte back), set the read timeout to 2 seconds, and curl it:

Two details worth reading out: first, the elapsed time is 2.005 seconds — exactly 2 seconds after the request reached the backend, Nginx cut it off, not waiting a single extra second; that is the precision of the read timeout. Second, the status code is 504 Gateway Time-out, not 502. 504 = the backend is alive but too slow (timed out); 502 = the backend connection was broken or returned an invalid response. In troubleshooting, split those two codes at first glance.
The reverse verification is also valuable: with the same configuration, swap the backend for a normally responding service, and the identical request returns in milliseconds:

A 200 with 0.7 milliseconds of elapsed time proves the only variable behind the 504 was “the backend didn’t emit response headers within 2 seconds” — not any other parameter in the config.
This silent backend itself deserves a place in your toolbox: sleep 60 | nc -l -p 9001 is one line that produces a “take the request but never answer” service — useful for verifying proxy-layer timeouts and testing whether your monitoring alerts actually catch a 504, and far cleaner than burying sleeps inside an application. Latency inside application code tends to spread across multiple stages; this gives you a pure timeout scene.
Example 3: keepalive_timeout’s Second Parameter
keepalive_timeout has a rarely used second parameter, made specifically to tell the client “how much longer this connection can be reused”:
1 | keepalive_timeout 60s 15s; |
The first parameter, 60s, is how long Nginx actually keeps an idle connection open; the second, 15s, is written into the response header. Testing the response headers of a simple endpoint:
1 | curl -si http://127.0.0.1/ka |
Live response headers:

Connection: keep-alive and Keep-Alive: timeout=15 appear together — the second parameter is in effect. A browser reading this header knows it can reuse the connection directly within 15 seconds. The second parameter should be less than or equal to the first, letting the client proactively switch to a fresh connection before the server closes it — smoother than slamming into a server-side forced close and reconnecting.
Note: under HTTP/1.1 connection reuse is the default behavior, and keepalive_timeout 0 falls back to the short-connection mode of “one request per connection” — not recommended unless you are load testing or working in a special intermediate-layer scenario.
Two Typical Misconfigurations
Mistake one: the backend is slow, so tune keepalive_timeout
1 | # Wrong: the backend processes slowly; tuning the client keep-alive does nothing |
What’s wrong: keepalive_timeout guards the client↔Nginx idle connection; however slow the backend is, it never enters that clock. After tuning, timeouts happen as before — plus client connections now hang around uselessly for 10 extra minutes.
Mistake two: cranking the read timeout blindly to 1 hour
1 | # Wrong: a failing request pins a worker for a full hour |
What’s wrong: every request waiting on a backend response occupies a worker connection. Once the backend stumbles, hundreds or thousands of requests queue up for 3600 seconds each, the worker pool drains fast, and healthy requests can’t get in either — one slow backend drags down the whole Nginx.
Caveats
- Bigger is not better for timeout parameters. A read timeout stretched too long lets failing requests hold worker connections for ages, which slows overall service at peak instead. Setting it at the backend’s real P99 plus headroom is sturdier than a uniform max-out.
- Connect-class and read-class timeouts leave different log signatures — that is the troubleshooting key.
upstream timed out ... while connecting to upstreambasically means the backend process is down or the network is broken;while reading response headermeans the backend is alive but processing too slowly. Recognize the suffix in the log first and the direction is instantly half-cleared. - Don’t mix up the directions.
keepalive_timeoutgoverns client-side connection reuse; the threeproxy_*parameters govern the Nginx→backend direction. The two segments are unrelated — before tuning anything, ask “between whom does this timeout occur?” - The firing conditions of
proxy_connect_timeoutandproxy_send_timeoutwere not live-tested in this post: in a single-machine/container environment, connection setup either succeeds immediately or is refused immediately — there is no way to fabricate a “silent packet drop” network path. Their behavior descriptions come from nginx’s official ngx_http_proxy_module documentation; in production they can usually stay at the default 60s or be trimmed slightly.
Wrap-up
Back to the opening 504: next time, open the error log first and read the suffix — while connecting sends you to check backend liveness and the network; while reading sends you to check backend latency and consider raising proxy_read_timeout. Four parameters, four guarded segments: keepalive_timeout for client reuse, proxy_connect_timeout for connection setup, proxy_read_timeout for waiting on responses, proxy_send_timeout for sending requests. Segment the path first, then touch parameters — timeout tuning turns from a coin toss into building by blueprint.
This post was rewritten from the author’s CSDN blog articles originally published between 2020 and 2024 on CSDN.