Preventing Django connection leaks during Celery tasks
This guide is part of Django Database Connection Management. Celery’s long-running worker processes bypass Django’s standard request-response lifecycle. ORM connections persist across tasks instead of closing after execution. Without explicit teardown, idle and idle-in-transaction states accumulate. The database pool limit is eventually reached. This triggers OperationalError: too many connections and cascading worker failures.
This guide delivers exact remediation steps, signal-based hooks, and validation commands. It enforces strict connection lifecycle management for production workloads.
Key operational realities:
- Celery workers reuse processes, bypassing Django’s per-request connection closure.
- Unclosed transactions and idle connections accumulate until DB pool limits are hit.
- Remediation requires explicit lifecycle hooks, connection closing signals, and pool validation.
Diagnosing Connection Exhaustion in Celery Workers
Identify symptoms and isolate leaked connections using database metrics and worker logs. Monitor pg_stat_activity for persistent idle-in-transaction states. Check Celery worker logs for DatabaseError or connection pool timeouts. Correlate connection spikes directly with task execution frequency.
Understanding how persistent processes bypass standard request teardown is critical. Review the Framework Integration & Connection Lifecycle documentation for architectural context on process reuse and connection retention.
Use the following thresholds to trigger incident response:
| Metric | Warning Threshold | Critical Threshold | Action |
|---|---|---|---|
idle connections |
> 60% of max_connections |
> 85% of max_connections |
Scale workers or force-close |
idle-in-transaction age |
> 30s | > 120s | Terminate backend PID |
| Celery connection errors | > 5/min | > 20/min | Drain queue, restart workers |
Query the database directly to isolate offending connections. Filter by application_name matching your Celery worker prefix. Cross-reference PIDs with active task logs.
Implementing Explicit Connection Teardown Hooks
Force Django to close connections after each Celery task completes. The task_postrun signal provides deterministic cleanup. Django’s connections.close_all() iterates over all configured database aliases and closes each connection. Handle transaction rollback explicitly on task failure before closing.
Align your implementation with established Django Database Connection Management best practices. This ensures ORM lifecycle control remains consistent across synchronous and asynchronous execution paths.
Deploy the following signal handler in your Celery configuration module:
from celery.signals import task_postrun
from django.db import connections
@task_postrun.connect
def close_db_connections(**kwargs):
"""
Deterministically close all thread-local Django DB connections
after Celery task execution to prevent pool exhaustion.
"""
connections.close_all()
connections.close_all() calls close() on every alias defined in settings.DATABASES. It is equivalent to iterating connections.all() and calling conn.close() on each — do not do both. Place this handler in a module that Celery imports at startup (e.g., your celery.py or an apps.py ready() hook).
If you prefer a more conservative approach that only closes unhealthy connections, use close_old_connections() instead:
from celery.signals import task_postrun
from django.db import close_old_connections
@task_postrun.connect
def recycle_db_connections(**kwargs):
"""
Close connections that are unusable or have exceeded CONN_MAX_AGE.
More conservative than close_all(); suitable when CONN_MAX_AGE > 0.
"""
close_old_connections()
Use close_all() when workers are short-lived or when you want a clean slate after every task. Use close_old_connections() when CONN_MAX_AGE is set and you want to preserve warm connections that are still valid.
Validating Pool Health and Connection Reuse
Verify remediation steps and ensure stable pool utilization under load. Execute synthetic task bursts to simulate production traffic. Track connection delta before, during, and after execution.
Run the following validation query against your database:
SELECT count(*), state
FROM pg_stat_activity
WHERE datname = 'your_db_name'
GROUP BY state;
This query quickly verifies if idle connections drop back to baseline after Celery task bursts. Monitor django.db.connections thread-local state post-task using Django’s debug toolbar or custom middleware.
Execute a controlled load test:
- Record baseline
pg_stat_activitycounts. - Dispatch 500 concurrent tasks via Celery.
- Wait for task completion queue to drain.
- Re-run the validation query.
- Confirm
idlecount returns to baseline ±10%.
Deviations indicate lingering references or unhandled transaction blocks. Audit task code for raw SQL cursors or third-party libraries bypassing the ORM.
Common Mistakes
| Issue | Root Cause | Operational Impact |
|---|---|---|
Relying solely on CONN_MAX_AGE |
Limits persistent connection lifetime only | Does not force closure between tasks. Idle connections remain until timeout expires. |
Calling close_old_connections() when CONN_MAX_AGE=None |
close_old_connections() only closes connections that exceed CONN_MAX_AGE; with None, no connection is considered old |
Leaves all idle connections open. Use close_all() instead. |
| Ignoring transaction rollback on failure | Failed tasks leave transactions open | DB holds row locks. Connection counts as active. Causes cascading deadlocks. |
FAQ
Why do Django connections leak in Celery but not in Gunicorn?
Does CONN_MAX_AGE=0 fix Celery connection leaks?
0 disables persistent connections entirely. It forces a new connection per query. This increases latency and DB handshake overhead. It does not address the underlying leak pattern caused by tasks never calling connection teardown.How do I verify connections are actually closing?
SELECT count(*) FROM pg_stat_activity WHERE state = 'idle' before and after a Celery burst. A stable or decreasing count confirms successful teardown. Monitor worker memory for concurrent validation.Related
- Django Database Connection Management — the parent guide covering Django’s full connection lifecycle and proxy alignment.
- Configuring CONN_MAX_AGE for Django and PgBouncer — set the reuse window that
close_old_connections()honors during teardown. - FastAPI SQLAlchemy Pool Configuration — equivalent worker-side session and pool handling for an async Python stack.
- PgBouncer Transaction vs Statement Pooling — proxy pool mode that bounds backend connections held by Celery workers.