Fix Django 500 Internal Server Error in Production

Find the real cause of a Django 500 error in production: where tracebacks go with DEBUG off, logging setup, common causes, safe reproduction, and verification.

Problem statement

A Django site in production shows "Server Error (500)" for some or all pages. With DEBUG = False, Django deliberately hides the traceback from visitors, so the page itself tells you nothing. Many developers respond by turning DEBUG back on in production, which exposes settings, source code, and environment details to anyone who triggers an error.

A 500 response means Django (or the app server) caught an unhandled exception while processing the request. It is different from:

  • 502 Bad Gateway: the proxy could not get a valid response from the app server at all
  • 504 Gateway Timeout: the app server did not respond in time
  • 400 Bad Request: often DisallowedHost, which Django handles before your views run
  • 403 Forbidden: CSRF failures or permission checks

This guide shows how to get the traceback without enabling debug mode, lists the most common production causes, and walks through a safe fix-and-verify process.

Finding a hidden 500Requestreturns 500Djangoexception raisedLoggingdjango.requestTracebacklogs or trackerWith DEBUG off, the detail goes to logging, not to the visitor.

Quick answer

  1. Confirm it is a real Django 500, not a 502 or 504 from the proxy: check the Nginx access log status and the response body.
  2. Read the traceback from the app server logs (journalctl -u gunicorn, container logs, platform logs) or your error tracker.
  3. If there is no traceback anywhere, configure LOGGING so django.request errors go to stderr, restart, and reproduce.
  4. Match the exception to a cause: missing environment variable, database or migration problem, missing template or static manifest entry, permission error, or a bug in new code.
  5. Reproduce safely with python manage.py shell or a test using production-like settings, never by enabling DEBUG on the live site.
  6. Fix, deploy, and verify the endpoint and error rate.

Step-by-step solution

1. Confirm the status code and scope

curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/admin/login/
sudo tail -n 50 /var/log/nginx/access.log | awk '{print $9, $7}' | sort | uniq -c | sort -rn | head

Answer three questions:

  • Is every page failing, or only some URLs? Everything failing points to configuration, database, or startup problems. Specific URLs point to code or data problems.
  • Did it start after a deploy, a configuration change, or on its own? Unprompted failures suggest external dependencies (database, disk, third-party API).
  • Is the body Django's plain "Server Error (500)" page, your custom 500.html, or the proxy's error page? A proxy error page usually means 502 or 504, not a Django exception.

2. Find the traceback

With DEBUG = False, Django logs unhandled request exceptions through the django.request logger at ERROR level. Where that ends up depends on your logging setup and process manager.

where the traceback ends upError trackerSentry or similar, if configureddjango.request loggerto stdout, file, or ADMINS emailGunicorn error logif Django logging goes to stderrjournald or container logsjournalctl -u gunicornIf none of these has it, fix logging before anything elsedjango-deployment.com
A 500 with no traceback anywhere means logging is misconfigured.

systemd with Gunicorn:

sudo journalctl -u gunicorn --since "30 min ago" --no-pager | grep -B 5 -A 40 "Traceback"

Docker Compose:

docker compose logs --since 30m web | grep -A 40 "Traceback"

Platforms (Heroku, Render, Railway, Fly.io): use the platform's log command or dashboard for the web process.

Error tracker: if Sentry or a similar tool is configured, the event contains the traceback, request data, and release version.

If Django's default logging is in place, the mail_admins handler emails the traceback to ADMINS when DEBUG = False, so also check the admin mailbox if email is configured.

3. No traceback anywhere? Fix logging first

Gunicorn does not automatically show Django's log output unless Django logs to stderr. Add a minimal logging configuration:

# settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "verbose": {"format": "{asctime} {levelname} {name} {message}", "style": "{"},
    },
    "handlers": {
        "console": {"class": "logging.StreamHandler", "formatter": "verbose"},
    },
    "root": {"handlers": ["console"], "level": "WARNING"},
    "loggers": {
        "django.request": {"handlers": ["console"], "level": "ERROR", "propagate": False},
    },
}

Also make sure Gunicorn captures stderr. With systemd, stdout and stderr go to the journal by default. When running Gunicorn directly, --error-logfile - sends its own error log to stderr, and --capture-output redirects the application's stdout and stderr into that error log.

Restart, reproduce the failing request, and read the logs again.

4. Match the exception to a cause

The last lines of the traceback name the exception. The most common production causes:

Missing configuration

exception to causeExceptionUsual causeProgrammingError:relation does not existmigrations not appliedOperationalError: could not connectdatabase down or wrong hostValueError: Missingstaticfiles manifestcollectstatic not runTemplateDoesNotExisttemplate not in the releaseKeyError on os.environvariable missing in production envMost production-only 500s come from the deploy, not the codedjango-deployment.com
If it only fails in production, compare the release and environment first.
KeyError: 'STRIPE_API_KEY'
django.core.exceptions.ImproperlyConfigured: ...

An environment variable is missing in the web process. Compare the service's environment file with what the code expects, then restart the service.

Database errors

django.db.utils.ProgrammingError: relation "orders_order" does not exist
django.db.utils.ProgrammingError: column orders_order.status does not exist
django.db.utils.OperationalError: could not connect to server

relation ... does not exist or column ... does not exist almost always means migrations were not applied for the deployed code. Run python manage.py showmigrations with production settings and apply pending migrations. OperationalError points to connectivity or credentials.

Templates

django.template.exceptions.TemplateDoesNotExist: 500.html
django.template.exceptions.TemplateSyntaxError: ...

A template missing from the deployed code, an app not in INSTALLED_APPS, or a template error that only appears on a code path you did not test.

Static files manifest

ValueError: Missing staticfiles manifest entry for 'css/app.css'

With ManifestStaticFilesStorage (or WhiteNoise's compressed manifest storage), {% static %} raises this when collectstatic was not run for the current code, or the file path is wrong. Run collectstatic and check the path.

File permissions

PermissionError: [Errno 13] Permission denied: '/srv/myapp/media/uploads/...'

The app server user cannot write to MEDIA_ROOT or a log file path.

Code and data bugs

AttributeError: 'NoneType' object has no attribute 'email'

Code assumed data that exists in development but not in production, such as a user profile or a related object.

5. Reproduce safely

Load the production environment in a shell on the server (or a staging environment with the same code and schema) and call the failing code directly:

sudo -u myapp bash -c 'set -a; . /etc/myapp/env; set +a; \
  cd /srv/myapp/app && /srv/myapp/venv/bin/python manage.py shell'
reproduce without making it worseUnsafe✕DEBUG = True on the live site✕editing code on the server✕guessing with repeated deploysSafe✓staging with a data copy✓manage.py shell with prod settings✓a failing test first, then the fixDEBUG on production leaks settings to every visitordjango-deployment.com
A failing test also stops the bug from coming back.
from django.test import Client

client = Client(HTTP_HOST="www.example.com")
response = client.get("/orders/42/", secure=True)
print(response.status_code)

With DEBUG = False, the test client re-raises the view's exception in the shell by default (raise_request_exception=True), giving you the full traceback without exposing anything publicly. The request runs against the real database, so avoid reproducing requests that change data.

6. Fix and deploy

Apply the fix appropriate to the cause:

  • missing variable: add it to the environment file or platform config, then restart
  • pending migrations: python manage.py migrate --noinput
  • manifest error: python manage.py collectstatic --noinput, then restart
  • permissions: correct ownership of the directory for the service user
  • code bug: deploy a fix, or roll back to the previous release if the fix is not quick

If many users are affected and the cause is a recent deploy, roll back first and investigate afterwards.

7. Verify

curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/orders/42/
sudo journalctl -u gunicorn --since "5 min ago" --no-pager | grep -c "Traceback"
python manage.py check --deploy

Verification check:

  • the failing URL returns the expected status
  • no new tracebacks appear in logs or the error tracker for that exception
  • the 5xx rate in access logs returns to baseline
  • DEBUG is still False

Explanation

Django separates what the visitor sees from what operators see. With DEBUG = False, visitors get a generic error page (your 500.html template if one exists), and details go to logging handlers. Most "mysterious" production 500 errors are only mysterious because logging is not wired to a place anyone reads. Once tracebacks are visible, the cause is usually one of a small set: configuration, schema, static manifest, permissions, or code that met real data.

Edge cases / notes

The 500 page itself fails

If 500.html extends a base template that uses context processors, database queries, or {% static %} with a missing manifest entry, rendering the error page fails too. Django's handler500 renders 500.html without a request context by default for that reason. Keep 500.html simple and self-contained.

Errors only for logged-in users or specific data

Middleware, context processors, or template tags that run for authenticated users can fail for accounts with unusual data. Reproduce with the same user in a staging copy or with the test client's force_login().

Intermittent 500 errors

Intermittent failures often come from external dependencies: database connection limits, timeouts to third-party APIs, or a full disk. Correlate error timestamps with those systems' metrics.

Common mistakes

  • Setting DEBUG = True in production "for five minutes" to see the error. Crawlers and attackers probe error pages constantly.
  • Catching all exceptions in views and returning 200 with an error message, which hides failures from monitoring.
  • Reading only Nginx logs, which show the status code but never the Python traceback.
  • Restarting the app repeatedly without reading logs; this can clear the symptom temporarily and lose evidence.

Next steps

FAQ

How do I see Django errors in production without DEBUG=True?

Configure LOGGING so the django.request logger writes errors to stderr or a file, and read them from your process manager's logs. An error tracker such as Sentry adds request context and alerts.

Why does Django show "Server Error (500)" with no details?

With DEBUG = False, Django hides exception details from visitors and renders a generic error page. The traceback is sent to the logging system instead.

What is the most common cause of Django 500 errors after a deploy?

Pending migrations and missing environment variables are the most frequent. Check showmigrations and compare the deployed configuration with what the new code requires.

Is it safe to turn on DEBUG briefly to debug a production 500?

No. Debug pages reveal settings, local variables, and source code to whoever triggers them. Reproduce the error in a shell with the test client or in staging instead.

Why do I get a 500 error only on pages that use static files?

With manifest-based static storage, {% static %} raises an error when a file is missing from the manifest. Run collectstatic for the deployed code and fix any wrong static paths.

Keep going