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:

  1. 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.
  2. connect() to unix:/run/gunicorn.sock failed (13: Permission denied): the socket exists but nginx's user can't use it.
  3. No such file or directory for the socket: the path in nginx and in gunicorn's --bind differ, or gunicorn hasn't created it.
  4. gunicorn is bound to 127.0.0.1 inside a container, and the proxy is in another container or on the host. Bind to 0.0.0.0.
  5. 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.
  6. 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

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

Get startedSee pricing

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.