gunicorn logs: where they go, access log, error log (tested)
By default gunicorn writes its error log (including 500 tracebacks) to stderr and writes no access log at all. Add --access-logfile - to log requests to stdout, --error-logfile path to write errors to a file, and --capture-output to send print() output from your app there too.
1. Where each kind of output goes
app.py prints, logs a warning, and raises on /boom:
import logging, sys
log = logging.getLogger("myapp")
def app(environ, start_response):
print("print() from the app")
log.warning("logger.warning from the app")
if environ["PATH_INFO"] == "/boom":
raise ValueError("boom")
start_response("200 OK", [("Content-Type", "text/plain")])
return [b"ok\n"]
run.py starts gunicorn three ways, requests / and /boom, and shows the terminal output and log files (startup lines filtered out):
import os, subprocess, time, urllib.request
G = "/tmp/fx384/ve/bin/gunicorn"
def trial(args):
for f in ("error.log", "access.log"):
if os.path.exists(f): os.remove(f)
cmd = [G, "-b", "127.0.0.1:8966", *args, "app:app"]
print("$ gunicorn", " ".join(cmd[1:]))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(1.5)
for path in ("/", "/boom"):
try: urllib.request.urlopen(f"http://127.0.0.1:8966{path}", timeout=10)
except Exception as ex: print(f" (GET {path} -> {ex})")
time.sleep(0.5); p.terminate(); out = p.communicate()[0]
print(" --- terminal ---")
for line in out.splitlines():
if any(k in line for k in ("Starting gunicorn", "Listening", "Using worker", "Booting", "Control socket", "Handling signal", "Worker exiting", "Shutting down", " File", " ")): continue
print(" " + line)
for f in ("error.log", "access.log"):
if os.path.exists(f):
print(f" --- {f} (grep app/GET/Error) ---")
for line in open(f):
if any(k in line for k in ("app", "GET", "Error", "boom")): print(" " + line.rstrip())
print()
trial([])
trial(["--access-logfile", "-"])
trial(["--access-logfile", "access.log", "--error-logfile", "error.log", "--capture-output"])
Output (gunicorn 26.2.0, Python 3.13.5, Linux):
$ gunicorn -b 127.0.0.1:8966 app:app
(GET /boom -> HTTP Error 500: Internal Server Error)
--- terminal ---
print() from the app
logger.warning from the app
print() from the app
logger.warning from the app
[2026-10-23 20:34:21 +0100] [3408698] [ERROR] Error handling request GET /boom
Traceback (most recent call last):
ValueError: boom
$ gunicorn -b 127.0.0.1:8966 --access-logfile - app:app
(GET /boom -> HTTP Error 500: Internal Server Error)
--- terminal ---
print() from the app
logger.warning from the app
127.0.0.1 - - [23/Oct/2026:20:34:24 +0100] "GET / HTTP/1.1" 200 3 "-" "Python-urllib/3.13"
print() from the app
logger.warning from the app
[2026-10-23 20:34:24 +0100] [3408701] [ERROR] Error handling request GET /boom
Traceback (most recent call last):
ValueError: boom
127.0.0.1 - - [23/Oct/2026:20:34:24 +0100] "GET /boom HTTP/1.1" 500 0 "-" "-"
$ gunicorn -b 127.0.0.1:8966 --access-logfile access.log --error-logfile error.log --capture-output app:app
(GET /boom -> HTTP Error 500: Internal Server Error)
--- terminal ---
--- error.log (grep app/GET/Error) ---
print() from the app
logger.warning from the app
print() from the app
logger.warning from the app
[2026-10-23 20:34:26 +0100] [3408704] [ERROR] Error handling request GET /boom
File "/tmp/gl405/app.py", line 8, in app
raise ValueError("boom")
ValueError: boom
--- access.log (grep app/GET/Error) ---
127.0.0.1 - - [23/Oct/2026:20:34:26 +0100] "GET / HTTP/1.1" 200 3 "-" "Python-urllib/3.13"
127.0.0.1 - - [23/Oct/2026:20:34:26 +0100] "GET /boom HTTP/1.1" 500 0 "-" "-"
2. What this shows
- Default: the error log goes to stderr (your terminal, or
journalctl -u yourserviceunder systemd, ordocker logsin a container). The 500 traceback ([ERROR] Error handling request GET /boom) is there. There is no access log: the two GET requests left no line. --access-logfile -turns the access log on and sends it to stdout: one line per request with status (200, 500).--error-logfile error.log --access-logfile access.log --capture-outputsends everything to files and nothing to the terminal;--capture-outputis what put the app'sprint()and logger output intoerror.log.- A 500 from an unhandled exception is logged at
[ERROR]with the full traceback, and the frame in your code (app.py, line 8) shows the cause.
In gunicorn.conf.py the same settings are accesslog = "-", errorlog = "error.log", capture_output = True.
Related: gunicorn access log format examples, gunicorn 500 Internal Server Error, gunicorn unrecognized arguments, gunicorn [CRITICAL] WORKER TIMEOUT.
Frequently asked questions
Where are gunicorn logs stored?
Nowhere on disk by default: the error log goes to stderr (terminal, journalctl or docker logs) and the access log is off.
Why is the gunicorn access log not showing?
It is disabled by default. Pass --access-logfile - (stdout) or a file path, or set accesslog in gunicorn.conf.py.
How do I get print() output into the gunicorn error log file?
Use --capture-output together with --error-logfile.
Written with AI assistance. Measured on 2026-10-23 with gunicorn 26.2.0 and Python 3.13.5 on Linux. The script ran verbatim and the output was pasted unedited. PlainHeaders is our extension; gunicorn is free.