skip to content

In Django, what do the CONN_MAX_AGE and CONN_HEALTH_CHECKS database settings control, and what are their defaults?

level: middleimportance: should knowfreq 48%

answer

  1. per-thread connection lifetime
  2. zero means close every request
  3. None means never expire
  4. checked once, only if used
  5. request signals do the closing

basics

~20 s

CONN_MAX_AGE is how long, in seconds, a thread keeps its database connection for reuse across requests (default 0, closed after every request; None means unlimited); CONN_HEALTH_CHECKS, default False, checks a reused connection before a request's first query.

solid answer

~50 s

By default Django opens a connection on the first query of a request and closes it when the request ends: `CONN_MAX_AGE = 0`. A positive number of seconds makes connections **persistent**: each worker thread keeps its own connection and reuses it until it is older than the limit; `None` means no limit. Closing happens on the `request_started` and `request_finished` signals, which also drop connections left in an error state or with changed autocommit. `CONN_HEALTH_CHECKS = True` (default `False`) pings a reused connection once per request, only if that request touches the database, so a server restart does not fail the first request. Caveats: you need at least as many database connections as worker threads, the development server's thread per request makes persistence pointless, and Django's docs say to disable persistent connections under ASGI and use pooling instead.

code

python · 10 lines
python
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "timetable",
        "USER": "timetable_app",
        "HOST": "db.internal",
        "CONN_MAX_AGE": 60,
        "CONN_HEALTH_CHECKS": True,
    }
}

go deeper

for a junior

Recall that CONN_MAX_AGE defaults to 0, meaning a new connection per request, and that None keeps connections open forever.

for a middle

Explain per-thread persistence, closing on request_started and request_finished, and what CONN_HEALTH_CHECKS checks and when.

for a senior

Size connections against worker threads, handle long-running commands and workers yourself, and know why persistence is disabled under ASGI.

for a principal

Decide between persistent connections, the built-in pool and an external pooler across services, with the database's connection limit as the shared budget.

## The problem these settings solve Opening a database connection is expensive: a network handshake, authentication, and backend setup such as the time zone. Django's historical behaviour opens one connection per request and closes it at the end. For a busy site, that setup can be a visible share of each request's time. `CONN_MAX_AGE` and `CONN_HEALTH_CHECKS`, both set per alias inside `DATABASES`, control whether and how Django reuses connections. ## `CONN_MAX_AGE` | Value | Behaviour | |---|---| | `0` (default) | Close the connection at the end of every request | | Positive integer | Keep it and reuse it until it is that many seconds old | | `None` | Keep it indefinitely (**unlimited persistent connections**) | Mechanics worth explaining: - **One connection per thread.** Django stores connections per thread, so each worker thread holds its own persistent connection. The database must accept at least as many connections as you have worker threads across all processes. - **Lazy opening.** A connection is opened on the first query that needs it, not at startup. - **Closing is signal-driven.** `close_old_connections()` runs on `request_started` and `request_finished`. It closes a connection that is past its maximum age, one whose autocommit setting was left changed, and one that saw database errors and no longer works. Thus a broken connection affects at most one request per worker thread. - **Outside requests nothing happens automatically.** A management command or a background worker loop never sends those signals, so it keeps whatever connection it has; long-running loops should call `django.db.close_old_connections()` themselves between units of work. ## `CONN_HEALTH_CHECKS` Added in Django 4.1, default `False`. With persistent connections, a connection can be dead without Django knowing: the database restarted, or a network device dropped an idle socket. Without checks, the first query on it fails and that request errors. With `CONN_HEALTH_CHECKS = True`: - Before a request's first query on a reused connection, Django checks it is usable and transparently reconnects if not. - The check runs **once per request** and **only if the request accesses the database**, so it costs little. - It does not help when the server is actually down; it handles connections that went stale while the server is healthy. ## When persistence is the wrong tool 1. **Development server.** It handles each request in a new thread, so connections are never reused; do not enable persistence there. 2. **Rarely used databases.** An alias most views never touch (an external system's database) should keep a low value or `0`, so idle connections do not pile up. 3. **ASGI.** Django's documentation says persistent connections should be disabled under ASGI; use the backend's built-in connection pool instead (PostgreSQL's `OPTIONS["pool"]`) or an external pooler. 4. **Low-traffic sites behind an idle timeout.** If the database or a proxy kills idle connections, set `CONN_MAX_AGE` below that timeout, or turn on health checks. ## Choosing a value There is no universal number, but the reasoning is consistent: - Start with a value in the tens of seconds to a few minutes for a WSGI deployment with steady traffic, and turn on `CONN_HEALTH_CHECKS`. - Keep it **below** any idle timeout enforced by the database or a network device in between, so Django retires connections before something else kills them. - Use `None` only when you control both ends and have checked that the server's connection limit covers every thread of every process at once. - Use `0` for aliases that are rarely used, for ASGI, and whenever the built-in pool is enabled, since pooling requires it. ## Session state leaks With persistent connections Django configures a connection only when it opens it. If code changes connection-level state (isolation level, time zone, a role), that state survives into the next request on the same thread. Restore it, set it at the start of every request, or keep `CONN_MAX_AGE = 0` for that alias.

  • Why doesn't CONN_MAX_AGE help on the development server?
    `runserver` handles each request in a new thread, and Django keeps connections per thread. A new thread has no connection to reuse, so every request opens one regardless of `CONN_MAX_AGE`. Django's docs advise not enabling persistent connections in development.
  • A nightly management command runs for hours and starts failing after the database restarts; why doesn't CONN_HEALTH_CHECKS save it?
    Health checks and age-based closing run from `close_old_connections()`, which Django connects to `request_started` and `request_finished`. A management command sends neither, so its connection is never checked or replaced. Call `close_old_connections()` between units of work, or reconnect on `OperationalError`, in long-running processes.

saying these in an interview costs you the question

  • CONN_MAX_AGE defaults to None, so connections persist by default
  • One persistent connection is shared by all threads in a process
  • CONN_HEALTH_CHECKS pings the database before every query
  • Persistent connections are recommended under ASGI
  • Management commands close old connections after each loop automatically