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, with curl verifying 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_timeout does 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_timeout governs 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_timeout is 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name example.com;

location / {
# Forward to the backend server
proxy_pass http://backend_server;
# Timeout for establishing the connection to the backend
proxy_connect_timeout 30s;
# Timeout waiting for response data from the backend
proxy_read_timeout 300s;
# Timeout for sending the request body to the backend
proxy_send_timeout 30s;
}

# Idle keep-alive duration for client connections
keepalive_timeout 300s;
}

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:

Read timeout triggering a 504, live test

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:

Reverse verification

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:

keepalive second parameter, live test

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
2
3
4
5
# Wrong: the backend processes slowly; tuning the client keep-alive does nothing
keepalive_timeout 600s;

# Right: a slow backend lives on the Nginx←backend segment
proxy_read_timeout 600s;

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
2
3
4
5
# Wrong: a failing request pins a worker for a full hour
proxy_read_timeout 3600s;

# Right: leave headroom based on the backend's real P99 latency, not gut feeling
proxy_read_timeout 300s;

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 upstream basically means the backend process is down or the network is broken; while reading response header means 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_timeout governs client-side connection reuse; the three proxy_* 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_timeout and proxy_send_timeout were 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.