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.
Quick answer
- 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.
- Read the traceback from the app server logs (
journalctl -u gunicorn, container logs, platform logs) or your error tracker. - If there is no traceback anywhere, configure
LOGGINGsodjango.requesterrors go to stderr, restart, and reproduce. - 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.
- Reproduce safely with
python manage.py shellor a test using production-like settings, never by enablingDEBUGon the live site. - 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.
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
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'
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
DEBUGis stillFalse
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 = Truein production "for five minutes" to see the error. Crawlers and attackers probe error pages constantly. - Catching all exceptions in views and returning
200with 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
- If the site only breaks with debug off, see Django Works with DEBUG=True but Fails with DEBUG=False.
- Make logs reliable with Django Logging Setup for Production.
- Capture every exception with context using Set Up Sentry Error Monitoring for Django.
- If the proxy reports errors instead, see Django 502 Bad Gateway: Causes and Fixes.
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.