Tune Nginx Keepalive and Worker Connections for Django
Tune Nginx for a Django backend: worker_processes and worker_connections, client keepalive_timeout, upstream keepalive connection pools to Gunicorn or Uvicorn, proxy buffering, and file descriptor limits, with when each setting matters.
Problem statement
Nginx defaults handle modest traffic well, but under load a few settings decide whether it stays efficient: how many connections each worker accepts, how long idle client connections are kept, whether Nginx reuses connections to the Django backend, and how responses are buffered. Misconfigured, they show up as worker_connections are not enough errors, ephemeral port exhaustion, extra latency from new TCP connections to Gunicorn, or 502s from keepalive races. Tuning them is quick once you know which apply to your setup.
worker_processes auto;andworker_connections 4096;(raiseworker_rlimit_nofileand the systemdLimitNOFILEto match).- Client keepalive:
keepalive_timeout 30s;andkeepalive_requests 1000;are sensible for browsers. - Upstream keepalive helps with TCP backends (Uvicorn, Gunicorn gthread/async, remote app servers):
keepalive 32;in theupstreamblock plusproxy_http_version 1.1;andproxy_set_header Connection "";. - Do not enable upstream keepalive to Gunicorn sync workers; they close after each response.
- Unix sockets to a local backend avoid TCP overhead entirely; upstream keepalive matters less there.
- Keep
proxy_buffering on(default) for normal views; disable only for streaming endpoints.
Step-by-step solution
1. Workers and file descriptors
# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
}
# systemctl edit nginx
[Service]
LimitNOFILE=65535
A proxied request uses two connections (client and upstream), so each worker serves roughly worker_connections / 2 concurrent proxied requests. The log message worker_connections are not enough means raise it (and file limits).
2. Client keepalive
http {
keepalive_timeout 30s;
keepalive_requests 1000;
}
Browsers reuse connections for many requests (assets, API calls). Very long timeouts keep idle connections and file descriptors open; 15-60 seconds is a reasonable range. With HTTP/2, one connection per client carries all requests anyway.
3. Upstream keepalive to TCP backends
upstream django_app {
server 127.0.0.1:8000;
keepalive 32; # idle connections kept per worker
keepalive_timeout 60s;
}
server {
location / {
proxy_pass http://django_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Without this, Nginx opens a new TCP connection to the backend for every request; under heavy load that adds latency and can exhaust ephemeral ports (TIME_WAIT buildup). proxy_http_version 1.1 and clearing Connection are required, or Nginx sends Connection: close.
The backend must keep connections open longer than Nginx's upstream keepalive_timeout, or Nginx occasionally reuses a connection the backend just closed, producing intermittent 502s. Uvicorn's --timeout-keep-alive and Gunicorn's keepalive (for non-sync workers) control that.
4. Buffering and body handling
proxy_buffering on;
proxy_buffers 16 16k;
proxy_buffer_size 16k;
client_max_body_size 25m;
client_body_buffer_size 128k;
Buffering lets Nginx absorb the response quickly and free the Gunicorn worker, then feed slow clients itself, which is one of the main reasons to put Nginx in front of sync workers. Increase proxy_buffer_size if you see "upstream sent too big header" (large cookies or headers).
Verification check: under a load test, the Nginx error log shows no worker_connections are not enough, ss -s shows no runaway TIME_WAIT counts on the backend port, and latency to the backend drops with upstream keepalive enabled (compare $upstream_connect_time in logs).
Explanation
Nginx is event-driven: each worker process handles thousands of connections, limited by worker_connections and the process's file descriptor limit. Client keepalive avoids repeated TCP and TLS handshakes for browsers. Upstream keepalive is a separate connection pool from Nginx to backends; it removes per-request TCP setup but only works if the backend speaks persistent HTTP/1.1 connections, which Gunicorn's sync workers deliberately do not. Buffering decouples slow clients from app workers, which is often the biggest performance benefit Nginx brings to Django.
Edge cases / notes
- Load balancers in front (ALB, Cloudflare) also hold keepalive connections to Nginx; keep Nginx's client timeout longer than theirs.
- HTTP/2 to clients reduces client connections;
http2_max_concurrent_streamslimits per-connection concurrency. - Logging cost. At very high request rates, buffered access logs (
buffer=64k flush=5s) reduce disk writes. - Rate limiting zones also consume memory; size
limit_req_zoneappropriately.
Next steps
- Start from a solid base with Configure Nginx for Django in Production.
- Review everything with the Nginx Production Checklist for Django.
FAQ
Should I enable upstream keepalive to Gunicorn?
Only for non-sync workers (gthread, gevent, Uvicorn). Sync workers close connections after each response.
What does worker_connections are not enough mean?
Nginx hit its per-worker connection limit. Raise worker_connections and the file descriptor limits.
What keepalive_timeout should I use for clients?
Between 15 and 60 seconds suits most sites.
Do I need upstream keepalive with a Unix socket?
Rarely. Socket connections are cheap; the gain is small.