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.

Connections through NginxClientskeepalive to NginxNginx workersworker_connectionsUpstream keepalivereuse to backendGunicorn / Uvicornsocket or TCPClient-side and upstream-side keepalive are separate settings with different trade-offs.
  1. worker_processes auto; and worker_connections 4096; (raise worker_rlimit_nofile and the systemd LimitNOFILE to match).
  2. Client keepalive: keepalive_timeout 30s; and keepalive_requests 1000; are sensible for browsers.
  3. Upstream keepalive helps with TCP backends (Uvicorn, Gunicorn gthread/async, remote app servers): keepalive 32; in the upstream block plus proxy_http_version 1.1; and proxy_set_header Connection "";.
  4. Do not enable upstream keepalive to Gunicorn sync workers; they close after each response.
  5. Unix sockets to a local backend avoid TCP overhead entirely; upstream keepalive matters less there.
  6. 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.

two kinds of keepaliveSettingRecommendationClient sidekeepalive_timeoutโœ“15-60 sUpstream to Uvicorn / gthreadupstream keepalive Nโœ“enable, HTTP/1.1Upstream to sync Gunicornupstream keepalive!leave offUpstream via Unix socketupstream keepaliveoptional, small gainUpstream keepalive needs a backend that keeps connections opendjango-deployment.com
Match upstream keepalive to what the backend supports, or you trade latency for 502s.

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).

Nginx tuning for Djangoworker_connections and nofile raised togetherNginx and systemd limitsClient keepalive 15-60 snot indefiniteUpstream keepalive only for capable backendsUvicorn, gthread, remoteBackend keepalive longer than Nginxavoids 502 racesproxy_buffering on for normal viewsfrees workers fastMost Django sites need only a few of these; tune what your metrics showdjango-deployment.com
Check the Nginx error log for worker_connections and header size warnings first.

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_streams limits 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_zone appropriately.

Next steps

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.

Keep going