gunicorn access log format: --access-logformat examples (tested)
gunicorn writes access lines only with --access-logfile, in the format set by --access-logformat. Below: the default line and three custom formats (milliseconds, a request header, JSON-style), each run against a real request, plus what a typo in a key does.
1. Four access-log formats, measured
b1.sh starts gunicorn on a minimal WSGI app with --access-logfile - and different --access-logformat strings, sends one request with an X-Request-ID header, and prints the log line:
# start gunicorn with an access-log format, send one request, print what it logged
run() {
gunicorn app:app -b 127.0.0.1:8965 --access-logfile - --error-logfile err.log "$@" > access.log 2>&1 &
pid=$!; until curl -s -o /dev/null -H 'X-Request-ID: abc-123' 'http://127.0.0.1:8965/items?id=7'; do sleep 0.2; done
kill $pid; wait $pid 2>/dev/null; cat access.log
}
echo '== default format'; run
echo '== custom: time in ms, request id header, status'
run --access-logformat '%(h)s "%(r)s" %(s)s %(M)sms id=%({x-request-id}i)s'
echo '== JSON-style'
run --access-logformat '{"ip":"%(h)s","path":"%(U)s","query":"%(q)s","status":%(s)s,"secs":%(L)s}'
echo '== unknown key %(foo)s'
run --access-logformat '%(h)s %(foo)s %(s)s'
echo '== error log line format'; head -1 err.log
Output (gunicorn 26.2.0, Python 3.13):
== default format
127.0.0.1 - - [23/Oct/2026:23:09:53 +0100] "GET /items?id=7 HTTP/1.1" 200 2 "-" "curl/8.14.1"
== custom: time in ms, request id header, status
127.0.0.1 "GET /items?id=7 HTTP/1.1" 200 0ms id=abc-123
== JSON-style
{"ip":"127.0.0.1","path":"/items","query":"id=7","status":200,"secs":0.000271}
== unknown key %(foo)s
127.0.0.1 - 200
== error log line format
[2026-10-23 23:09:53 +0100] [3562812] [INFO] Starting gunicorn 26.2.0
2. What the output shows
- The default access format is Apache "combined"-style: client, time, request line, status, response length, referer, user agent.
%(M)sis the request time in milliseconds,%(L)sin decimal seconds; a fast request shows0msbut0.000271seconds.- Request headers are
%({header-name}i)sin lower case:%({x-request-id}i)sprintedabc-123. %(U)sis the path without the query string,%(q)sthe query string,%(r)sthe full request line.- An unknown key such as
%(foo)sdoes not raise an error: it is logged as-, so a typo fails silently. --access-logformatonly changes access lines. Error-log lines keep the[time] [pid] [LEVEL] messageformat shown last; changing that needs a logging config (--log-configorlogconfig_dict).
3. In gunicorn.conf.py
The same settings as a config file: accesslog = "-" and access_log_format = '%(h)s "%(r)s" %(s)s %(M)sms'. Without accesslog (or --access-logfile) there are no access lines at all, whatever the format.
Related: where gunicorn logs go, gunicorn unrecognized arguments, gunicorn WORKER TIMEOUT.
Frequently asked questions
How do I log request time in gunicorn?
Use %(M)s for milliseconds or %(L)s for decimal seconds in --access-logformat.
How do I log a request header in gunicorn?
Use %({header-name}i)s with the header name in lower case, e.g. %({x-request-id}i)s.
What happens with an unknown key in gunicorn access_log_format?
It is printed as "-"; there is no error, so check the output after changing the format.
Written with AI assistance. Measured on 2026-10-23 with gunicorn 26.2.0 and Python 3.13 on Linux. The script ran verbatim and the output was pasted unedited. PlainTables is our extension; gunicorn is free.