Guides / 502 Bad Gateway with Django, gunicorn and nginx
502 Bad Gateway with Django, gunicorn and nginx
Your site shows 502 Bad Gateway. nginx (or a load balancer) is up, but it can't get an answer from Django. The proxy isn't the problem: the app process behind it isn't running, isn't listening where the proxy looks, or died. The proxy's error log says which.
What it means
A 502 means a gateway received an invalid response, or none, from the server behind it. With Django that server is gunicorn (or uvicorn). A 504 is similar but means it did not answer in time; see gunicorn WORKER TIMEOUT.
Why it happens
Find the matching line in the proxy error log (/var/log/nginx/error.log) and the app's log:
connect() failed (111: Connection refused) while connecting to upstream: nothing listens on that address. gunicorn isn't running, crashed at startup, or listens on a different port or socket.connect() to unix:/run/gunicorn.sock failed (13: Permission denied): the socket exists but nginx's user can't use it.No such file or directoryfor the socket: the path in nginx and in gunicorn's--binddiffer, or gunicorn hasn't created it.- gunicorn is bound to
127.0.0.1inside a container, and the proxy is in another container or on the host. Bind to0.0.0.0. - The app crashes on startup: an import error, missing dependency, bad settings. The 502 is just the symptom. Check the settings module and missing modules.
Worker (pid:123) was sent SIGKILL! Perhaps out of memory?: the kernel killed gunicorn because the server ran out of memory.
The fix
Check in this order, on the machine that runs the app:
systemctl status gunicorn # is it running?
journalctl -u gunicorn -n 100 --no-pager # why did it stop?
ss -ltnp | grep -E ':8000|gunicorn' # what is listening, and where?
curl -i http://127.0.0.1:8000/healthz/ # does the app answer directly?
If the app answers on curl but nginx still returns a 502, the proxy points at the wrong place. A correct pairing looks like this:
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
For a unix socket, nginx's user needs access: run gunicorn with --bind unix:/run/gunicorn.sock and match the path in proxy_pass http://unix:/run/gunicorn.sock;. For out-of-memory kills, reduce workers or move to a larger server; see the memory line in dmesg | grep -i kill. If you only see 502s during deploys, the old process stopped before the new one was ready: use a graceful reload (systemctl reload gunicorn, which sends SIGHUP) or let the load balancer wait for a health check.
Making it stick in production
- Let a supervisor restart gunicorn:
Restart=alwaysin systemd, or a container restart policy. - Add a health check that proves the app answers (health check failing), and route traffic only to healthy instances.
- Send logs to one place you can search, and alert on 5xx rates.
- Watch memory. Set
--max-requests 1000 --max-requests-jitter 100to recycle workers that slowly leak.
Or skip it
Django Cloud puts your app behind a load balancer with HTTPS and waits for a health check before a release goes live. If a release fails to start, the previous one keeps serving, so a bad deploy doesn't become a 502.
pip install djangocloud-cli
djangocloud deploy
Free plans are available for students, AWS Activate startups, open-source, non-profit and research projects. See how Django Cloud compares with other ways to host Django.
Django is a registered trademark of the Django Software Foundation. Django Cloud is not affiliated with or endorsed by it. Found a mistake in this guide? Tell us and we will fix it.