Gunicorn Production Checklist for Django

A production checklist for running Django on Gunicorn: bind address and socket, worker class and count, timeouts and graceful shutdown, worker recycling, logging, forwarded headers, process supervision, and the proxy in front.

Problem statement

Gunicorn runs most production Django sites, and its defaults are deliberately simple: one sync worker, a 30-second timeout, logs to stderr. Real deployments need deliberate choices for workers, timeouts, recycling, logging, and supervision, and a reverse proxy in front. This checklist collects those choices in one place so a new deployment or a review of an existing one can confirm nothing important is left at a default that does not fit.

Gunicorn in productionBindsocket behind proxyWorkersclass, count, memoryTimeoutskill hung, drainSupervisedsystemd or containerA reviewed config, a proxy in front, and supervision make Gunicorn dependable.
  1. Bind to a Unix socket or 127.0.0.1 behind Nginx or Caddy (or 0.0.0.0:$PORT in containers behind a load balancer).
  2. Choose workers by CPU and memory, gthread for some I/O overlap, and measure per-worker memory.
  3. Set timeout, graceful_timeout, and keepalive explicitly, consistent with proxy timeouts.
  4. Recycle workers with max_requests plus jitter.
  5. Log access and errors to stdout/stderr with request duration.
  6. Supervise with systemd (or the container runtime) and reload gracefully on deploy.

Checklist

Configuration and binding

  • Configuration lives in a committed gunicorn.conf.py, not a long command line.
  • Gunicorn listens on a socket or loopback address; only the proxy is public.
  • forwarded_allow_ips lists only your proxy addresses.
  • The process runs as an unprivileged user, never root.

Workers

  • Worker count fits memory: workers × peak RSS plus other services stays under RAM.
  • Worker class matches the app: sync or gthread for typical Django, Uvicorn workers for ASGI, gevent only with care.
  • max_requests (for example 1000) and max_requests_jitter (100) are set.
  • preload_app is a deliberate choice with post-fork connection handling if enabled.
settings to set explicitlyTypical valueWhybindunix socket / 127.0.0.1only proxy is publicworkers / threadsmemory-bound, 2 threadsparallelism within RAMtimeout30-60 skill hung requestsgraceful_timeout30 sdrain on deploymax_requests1000 + jittercontain memory growthExplicit values make behaviour the same on every serverdjango-deployment.com
Print the effective configuration with --print-config to confirm.

Timeouts and shutdown

  • timeout is above normal request time and below the proxy's read timeout.
  • graceful_timeout covers in-flight requests during reloads; systemd TimeoutStopSec or container grace period is longer.
  • keepalive is only relevant for non-sync workers behind a proxy that reuses connections.
  • Slow work (exports, imports, reports) runs in Celery, not in requests.

Logging and observability

  • accesslog = "-" and errorlog = "-" with a format including request duration.
  • Application logging goes to stdout through Django's LOGGING.
  • Worker timeouts and OOM kills are alerted on (search for WORKER TIMEOUT and kernel OOM messages).

Supervision and deploys

  • systemd unit with Restart=on-failure, RuntimeDirectory for the socket, and an EnvironmentFile; or a container with a restart policy.
  • Deploys use systemctl reload (HUP) for code changes, or a full restart with preloading.
  • Nginx or Caddy in front handles TLS, static files, buffering, and body size limits.
before going liveCommitted gunicorn.conf.pyone source of truthPrivate bind, proxy in frontTLS, static, bufferingWorkers sized to memorymax_requests with jitterTimeouts aligned with proxygraceful drain on deploySupervised, logs to stdoutrestart on failureFive checks cover most Gunicorn incidentsdjango-deployment.com
Review this list again whenever traffic or server size changes.

Explanation

Gunicorn's job is narrow: run a pool of Django processes, give each request to a worker, and keep the pool healthy. Most production incidents involving it come from mismatched sizing (too many workers for memory), mismatched timeouts across layers, unbounded memory growth, or missing supervision. Making each of those an explicit, reviewed setting turns Gunicorn into a predictable component, with the proxy handling the parts it is not designed for.

Edge cases / notes

  • Containers: set GUNICORN_WORKERS explicitly; CPU counts inside containers can reflect the host.
  • PaaS: platforms set PORT; bind to it and keep the rest of the config the same.
  • ASGI: Uvicorn workers under Gunicorn follow the same checklist, with WebSocket timeouts at the proxy.
  • Windows: Gunicorn does not run on Windows; use Waitress.

Next steps

FAQ

How many Gunicorn workers should I run?

Start near 2-4 per CPU core, then limit by memory: workers times peak RSS must fit in RAM with other services.

What timeout should Gunicorn use?

Above your normal request time, typically 30-60 seconds, and below the proxy's read timeout.

Should Gunicorn face the internet directly?

No. Put Nginx, Caddy, or a load balancer in front for TLS, static files, and buffering.

Why use max_requests?

To recycle workers periodically and contain memory growth from leaks or large requests.

Keep going