Log Slow PostgreSQL Queries From Django With log_min_duration_statement
Turn on PostgreSQL slow query logging for a Django app: set log_min_duration_statement, add auto_explain for plans, tag queries with application_name and SQL comments to find the view that sent them, and ship logs to your log platform without flooding it.
Problem statement
pg_stat_statements tells you which query shapes are slow on average, but not the specific slow executions: which parameters, at what time, from which view or task. PostgreSQL can log every statement that exceeds a threshold, and with auto_explain it can log the plan as well. Combined with tags that identify the Django process and view, the slow query log becomes a direct path from a latency spike to the line of code responsible.
- Set
log_min_duration_statement = 500(milliseconds) as a starting threshold. - Add
application_nameper process type in Django'sOPTIONS(web,celery,cron). - Optionally enable
auto_explainwithauto_explain.log_min_duration = 2000for plans of very slow queries. - Tag queries with the originating view using sqlcommenter-style comments.
- Ship the PostgreSQL log to your log platform and alert on rate, not on individual entries.
Step-by-step solution
1. Enable the threshold
ALTER SYSTEM SET log_min_duration_statement = '500ms';
SELECT pg_reload_conf();
On managed services, set it in the parameter group (RDS), database flags (Cloud SQL), or server parameters (Azure). Statements slower than the threshold are logged with their duration and text. Start high (500 ms or 1 s) and lower it as you fix the worst offenders; a low threshold on a busy database floods the log.
2. Identify the source process
DATABASES["default"]["OPTIONS"] = {
"application_name": os.environ.get("DB_APP_NAME", "web"),
}
Set DB_APP_NAME=celery for workers and DB_APP_NAME=cron for scheduled jobs. Include it in the log prefix:
log_line_prefix = '%m [%p] app=%a user=%u db=%d '
Now each slow query line shows whether it came from the web tier or a worker.
3. Log plans with auto_explain
shared_preload_libraries = 'pg_stat_statements,auto_explain'
auto_explain.log_min_duration = '2s'
auto_explain.log_analyze = off
auto_explain.log_format = 'json'
shared_preload_libraries changes need a restart. Keep log_analyze off in production unless needed: with it on, timing instrumentation adds overhead to every query, not just slow ones. Managed services expose auto_explain as a loadable library on most plans.
4. Tag queries with the Django view
SQL comments travel with the statement into logs and pg_stat_activity. A small database wrapper adds them:
from django.db import connection
class QueryCommentMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
def comment(execute, sql, params, many, context):
match = getattr(request, "resolver_match", None) # set once URL routing has run
view = (match.view_name if match else "unresolved").replace("'", "")
return execute(f"{sql} /* view='{view}' */", params, many, context)
with connection.execute_wrapper(comment):
return self.get_response(request)
Libraries such as google-cloud-sqlcommenter or OpenTelemetry's sqlcommenter integration do this with trace IDs. Be aware that comments make otherwise identical statements distinct in some tools; pg_stat_statements normalizes comments away.
5. Ship and alert
Forward PostgreSQL logs (journald, files, or the provider's log stream) to your log platform. Build a chart of slow queries per minute by app and alert when the rate jumps, rather than on single entries. Review the top patterns weekly together with pg_stat_statements.
Explanation
log_min_duration_statement makes PostgreSQL log the full text and duration of any statement that runs longer than the threshold, at the moment it finishes. auto_explain hooks into the executor to log plans for slow statements, which is the closest you get to "what was the plan at that moment" in production. Tags via application_name and SQL comments bridge the gap between database logs and Django code, because the ORM's generated SQL alone rarely tells you which view produced it.
Edge cases / notes
- Sensitive data. Logged statements may include parameter values (emails, tokens). Treat database logs as sensitive.
- Log volume. Busy databases with a low threshold produce a lot of log data; sample or raise the threshold.
- Lock waits.
log_lock_waits = onlogs statements waiting longer thandeadlock_timeout, useful alongside slow query logs.
Next steps
- Prioritize with Find Slow Django Queries With pg_stat_statements.
- Diagnose individual queries with Use QuerySet.explain() and EXPLAIN ANALYZE.
- Stop runaway queries with Set PostgreSQL statement_timeout for Django.
FAQ
What threshold should I start with?
500 ms or 1 s on a busy database, then lower it gradually as you fix the slowest queries.
Does auto_explain slow down the database?
With log_analyze off, overhead is small. With it on, every query pays timing overhead.
How do I know which Django view sent a slow query?
Set application_name per process type and add SQL comments with the view name.
Is pg_stat_statements enough?
It shows which query shapes are costly, but not individual executions or their parameters; use both.