gunicorn 500 Internal Server Error: where the traceback is (tested)
A gunicorn 500 Internal Server Error means your app raised an exception; gunicorn itself is fine. The traceback is in gunicorn's error log (stderr by default) as Error handling request, or, for Flask, ERROR in app: Exception on /path. Both cases reproduced below.
1. Reproduce with a plain WSGI app
app.py raises on /boom:
def app(environ, start_response):
if environ['PATH_INFO'] == '/boom':
1 / 0 # unhandled exception in the app
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'ok']
b1.sh starts gunicorn, requests the failing URL, then prints the error log (startup/shutdown INFO lines filtered, timestamps shortened):
gunicorn app:app --bind 127.0.0.1:8966 --workers 1 > err.log 2>&1 &
until curl -s -o /dev/null 127.0.0.1:8966/; do :; done
curl -s -i 127.0.0.1:8966/boom | tr -d '\r'
echo; echo '--- gunicorn error log ---'
kill %1; wait
grep -v 'Starting\|Listening\|Using worker\|Booting\|Handling signal\|Shutting\|Worker exiting\|Goodbye\|Control socket' err.log | sed 's/^\[[^]]*\] \[[0-9]*\]/[time] [pid]/'
Output (gunicorn 26.2.0, Python 3.13):
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Type: text/html
Content-Length: 141
<html>
<head>
<title>Internal Server Error</title>
</head>
<body>
<h1><p>Internal Server Error</p></h1>
</body>
</html>
--- gunicorn error log ---
[time] [pid] [ERROR] Error handling request GET /boom
Traceback (most recent call last):
File "/tmp/fx384/ve/lib/python3.13/site-packages/gunicorn/workers/sync.py", line 141, in handle
self.handle_request(listener, req, client, addr)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/tmp/fx384/ve/lib/python3.13/site-packages/gunicorn/workers/sync.py", line 184, in handle_request
respiter = self.wsgi(environ, resp.start_response)
File "/tmp/g5416/app.py", line 3, in app
1 / 0 # unhandled exception in the app
~~^~~
ZeroDivisionError: division by zero
The browser only sees gunicorn's bare Internal Server Error page. The cause is in the error log: [ERROR] Error handling request GET /boom followed by the full traceback, ending in the exception from your code.
2. Same bug inside Flask
from flask import Flask
app = Flask(__name__)
@app.get('/boom')
def boom():
return 1 / 0
b2.sh is the same script with fapp:app on port 8967:
HTTP/1.1 500 INTERNAL SERVER ERROR
Server: gunicorn
Date: Fri, 23 Oct 2026 22:22:45 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 265
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
--- gunicorn error log ---
[2026-10-23 23:22:45,915] ERROR in app: Exception on /boom [GET]
Traceback (most recent call last):
File "/tmp/fx384/ve/lib/python3.13/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
File "/tmp/fx384/ve/lib/python3.13/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/tmp/fx384/ve/lib/python3.13/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
File "/tmp/fx384/ve/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/tmp/g5416/fapp.py", line 6, in boom
return 1 / 0
~~^~~
ZeroDivisionError: division by zero
This time Flask catches the exception and renders its own 500 page, so gunicorn never sees it: there is no Error handling request line. The traceback still lands in the same stream because Flask's default logger writes ERROR in app: Exception on /boom [GET] to stderr, which gunicorn's error log captures. Django behaves the same way: with DEBUG=False it returns its own 500 page and logs through the django.request logger (see Django 500 error with DEBUG=False).
3. How to find the cause
- Look in gunicorn's error log, not the access log: the access log only records the 500 status. By default the error log is stderr (systemd:
journalctl -u yourservice; Docker:docker logs). See where gunicorn logs go. - Search the log for
Error handling request(exception escaped the framework) orException on /path(Flask caught it). - If the log has
WORKER TIMEOUTinstead of a traceback, the worker was killed mid-request, which is a different problem: gunicorn WORKER TIMEOUT. - Fix the exception in your code; gunicorn itself is working correctly when it returns this 500.
Frequently asked questions
Why does gunicorn return 500 Internal Server Error?
An unhandled exception in your WSGI app. gunicorn logs it at ERROR level as "Error handling request GET /path" with the traceback in its error log.
Why is there no "Error handling request" line for my Flask app?
Flask catches the exception and returns its own 500 page; the traceback is logged by Flask as "ERROR in app: Exception on /path [GET]" to the same stderr stream.
Where is the gunicorn error log?
stderr by default (journalctl for systemd, docker logs for containers), or the file given by --error-logfile.
Written with AI assistance. Measured on 2026-10-23 with gunicorn 26.2.0, Flask 3.1.3 and Python 3.13 on Linux. The scripts ran verbatim and the output was pasted unedited (timestamps/pids masked by the script). PlainTables is our extension; gunicorn and Flask are free.