Fix Django InterfaceError: Connection Already Closed
Fix django.db.utils.InterfaceError connection already closed in Celery tasks, management commands, and long-running scripts: call close_old_connections, set CONN_MAX_AGE and CONN_HEALTH_CHECKS, and handle database restarts and idle timeouts.
Problem statement
A Celery worker, a management command that loops forever, or a custom consumer starts failing with:
django.db.utils.InterfaceError: connection already closed
Once it appears, every following task on that worker fails too, until the worker restarts. The database connection held by that process was closed, by the server, a network device, a database restart, or an earlier error, and Django keeps trying to use the dead connection object. In web requests, Django cleans up connections at the start and end of each request. Code that runs outside the request cycle does not get that cleanup unless you add it.
- Call
django.db.close_old_connections()before each unit of work in long-running loops. - In Celery, keep Django's integration enabled (it closes connections around tasks), and avoid holding connections in globals.
- Set
CONN_HEALTH_CHECKS = Trueso reused connections are tested first (Django 4.1+). - Keep
CONN_MAX_AGEbelow any idle timeout on the network path and the server. - After database failovers or restarts, expect one failure per worker; make tasks retry on
InterfaceErrorandOperationalError.
Step-by-step solution
1. Clean up in long-running loops
import time
from django.db import close_old_connections
def run_forever():
while True:
close_old_connections()
job = fetch_next_job()
if job:
process(job)
else:
time.sleep(5)
close_old_connections() closes connections that are unusable or older than CONN_MAX_AGE, which is exactly what Django does around each web request. Calling it at the top of every iteration means a connection that died during sleep is replaced before the next query.
2. Check Celery integration
Celery's Django fixup closes stale connections before and after each task when the app is configured with config_from_object("django.conf:settings") and Django is set up. Problems appear when:
- A task holds a connection or cursor in a module-level variable.
- A task runs for a long time and sleeps between queries (polling external APIs).
- Workers use the
threadsorgeventpool, where cleanup per task works differently.
For long tasks, call close_old_connections() before each batch of queries inside the task.
3. Enable health checks and a sensible lifetime
DATABASES["default"].update({
"CONN_MAX_AGE": 60,
"CONN_HEALTH_CHECKS": True,
})
With health checks, close_old_connections() and the request cycle test reused connections and replace broken ones. CONN_MAX_AGE should be shorter than server idle_session_timeout, PgBouncer's server_idle_timeout, and NAT or load balancer idle timeouts.
4. Retry tasks on connection errors
Even with cleanup, a connection can die mid-task during a failover. Make idempotent tasks retry:
from django.db import InterfaceError, OperationalError
@shared_task(bind=True, autoretry_for=(InterfaceError, OperationalError),
retry_backoff=True, max_retries=3)
def sync_invoices(self):
...
Celery reruns the task on a new attempt, by which time Django will have opened a fresh connection.
5. Do not keep cursors and connections around
# risky
CURSOR = connection.cursor()
# fine
def report():
with connection.cursor() as cursor:
cursor.execute("SELECT ...")
return cursor.fetchall()
A cursor object stored globally refers to whatever connection existed when it was created. After that connection is closed, every use raises InterfaceError.
Explanation
Django keeps one connection per thread per database alias and reuses it until something closes it. In a web request, request_started and request_finished signals call close_old_connections(), which discards connections that errored, exceeded CONN_MAX_AGE, or fail the health check. Background processes do not emit those signals. If the server closes the socket, the connection object remains, marked closed, and Django's wrapper raises InterfaceError on every use instead of silently reconnecting, because reconnecting inside an open transaction could hide data loss.
Edge cases / notes
- Inside atomic blocks. Django does not reconnect inside a transaction. Let the transaction fail and retry the whole unit of work.
- Threads. Each thread has its own connection; threads you start yourself must close their connections when they finish.
- Tests.
TransactionTestCaseand live server tests can show this error when threads share connections incorrectly.
Next steps
- For the network-level causes, see Fix Server Closed the Connection Unexpectedly.
- Forked workers sharing connections produce different errors: Fix SSL Decryption Failed or Bad Record MAC.
- Tune lifetimes with Use Persistent Database Connections in Django.
FAQ
Why does every task fail after the first error?
The worker keeps reusing the dead connection object. Without cleanup, nothing replaces it until the process restarts.
Does Django reconnect automatically?
At the start and end of web requests, yes. In other code, only when you call close_old_connections() or close the connection.
Should I set CONN_MAX_AGE to 0 for workers?
That closes connections after each request cycle, but workers do not have one. Cleanup calls matter more than the setting.
Is InterfaceError different from OperationalError here?
OperationalError usually appears on the first failed query. InterfaceError appears on later uses of the already-closed connection.