Keep-alive lets a browser reuse one TCP (and TLS) connection for many requests instead of opening a new one for every file. Timeouts decide how long each layer waits before giving up. Tuning keep-alive and timeouts together makes a server faster, more resistant to slow-client attacks, and stops confusing 502 and 504 errors.
Before you start
- Root access to a server running Nginx or Apache, usually with PHP-FPM.
- Know your longest legitimate request (imports, report exports, uploads).
Why keep-alive matters
A new HTTPS connection costs a TCP handshake plus a TLS handshake, one or more round trips before the first byte. Keep-alive pays that once per visitor. HTTP/2 goes further and multiplexes all requests over one connection, so the keep-alive timeout simply controls how long an idle connection stays open.
The cost: each idle connection holds memory, and on Apache prefork a whole process. That is why the right value differs by server.
The timeout chain
| Layer | Setting | What it limits |
|---|---|---|
| Nginx | client_header_timeout, client_body_timeout | Slow clients sending headers or upload body |
| Nginx | keepalive_timeout | Idle time between requests on a connection |
| Nginx | send_timeout | Gap between two writes to a slow client |
| Nginx | fastcgi_read_timeout / proxy_read_timeout | Waiting for PHP-FPM or a backend to answer |
| Apache | KeepAliveTimeout, Timeout, RequestReadTimeout | Idle keep-alive, I/O waits, slow headers/body |
| PHP-FPM | request_terminate_timeout | Hard kill of a running PHP request |
| PHP | max_execution_time | Script CPU time limit |
Align the timeouts
The rule: each inner layer should give up before the outer layer. Then the user gets a clear error from the layer that actually failed.
max_execution_time(e.g. 60) <request_terminate_timeout(e.g. 70) <fastcgi_read_timeout(e.g. 75).- If Nginx gives up first, you get 504 Gateway Timeout while PHP keeps running in the background and wastes a worker.
- If PHP-FPM kills the request first, Nginx logs "upstream prematurely closed connection" and returns 502.
For a single slow admin endpoint, raise timeouts in its own location rather than globally.
Starting values for Nginx
keepalive_timeout 30s;
keepalive_requests 1000;
client_header_timeout 15s;
client_body_timeout 30s;
send_timeout 30s;
fastcgi_read_timeout 75s;Nginx handles idle connections cheaply, so 30–65 seconds of keep-alive is fine.
Starting values for Apache
KeepAlive On
MaxKeepAliveRequests 500
KeepAliveTimeout 5
Timeout 60
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500With the event MPM a longer KeepAliveTimeout is cheap; with prefork keep it at 2–5 seconds so idle visitors do not occupy processes. RequestReadTimeout (mod_reqtimeout) is Apache's main defence against slowloris attacks.
Upstream keep-alive
Nginx can also keep connections open to backends, which helps when proxying to Node.js or another HTTP service:
upstream app { server 127.0.0.1:3000; keepalive 32; }
location / { proxy_pass http://app; proxy_http_version 1.1; proxy_set_header Connection ""; }Verify
- Check that connections are reused:
curl -sv https://example.com/ https://example.com/robots.txt 2>&1 | grep -i 're-using\|Connected'. - Watch connection states:
ss -sandss -tan state established | wc -l. - Look for timeouts in error logs after changes.
Troubleshooting
- 504 on long imports: raise
fastcgi_read_timeoutand PHP limits for that location only, or move the job to a background queue. - Thousands of idle connections on Apache prefork: lower KeepAliveTimeout or switch to the event MPM.
- Uploads fail midway:
client_body_timeouttoo short for slow connections.
FAQ: keep-alive and timeouts
Should I disable keep-alive?
Almost never. It is a large speed win. Only shorten it on memory-starved prefork servers.
Does HTTP/2 need keep-alive settings?
Yes, keepalive_timeout still closes idle HTTP/2 connections in Nginx.
Why does my site return 504 but the task still finishes?
Nginx stopped waiting before PHP finished. Align the timeout chain.
