Fix PostgreSQL "Too Many Connections" in Django

Fix PostgreSQL sorry, too many clients already errors from Django: count connections by source, fix CONN_MAX_AGE and worker math, find idle leaks, and add pooling.

Problem statement

Django requests start failing with:

django.db.utils.OperationalError: connection to server at "10.0.1.5", port 5432 failed:
FATAL:  sorry, too many clients already

or, on some managed services:

FATAL:  remaining connection slots are reserved for non-replication superuser connections

PostgreSQL has a hard limit (max_connections) on concurrent connections, and each connection is a server process with its own memory. Once the limit is reached, new connections are refused until existing ones close. The error often appears suddenly after scaling up web workers, adding servers, starting Celery workers, or enabling persistent connections, even though the database's CPU is idle.

Where connections come fromWeb workersthreads eachCelery workersconcurrencyToolsshells, cronPostgreSQLmax_connectionsPersistent connections make the steady-state count equal to your concurrency settings.

Quick answer

  1. Check the limit and current usage: SHOW max_connections; and a pg_stat_activity query grouped by application, user, and state.
  2. Calculate the connections your deployment can open: servers × Gunicorn workers × threads, plus Celery workers, schedulers, and admin tools.
  3. If many connections are idle, lower CONN_MAX_AGE or make sure persistent connections are not multiplied by threads.
  4. If connections are idle in transaction, find the code holding transactions open and set idle_in_transaction_session_timeout.
  5. Reduce total demand (fewer workers per server, smaller Celery concurrency) or add pooling (PgBouncer or Django's built-in psycopg pool).
  6. Avoid simply raising max_connections far beyond what the database memory supports.

Step-by-step solution

1. Measure current connections

SHOW max_connections;
SHOW superuser_reserved_connections;

SELECT count(*) FROM pg_stat_activity;

SELECT usename, application_name, client_addr, state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY usename, application_name, client_addr, state
ORDER BY count(*) DESC;

This shows which hosts and roles hold connections, and in which state:

  • active: running a query
  • idle: connected, waiting for the next query (persistent connections look like this)
  • idle in transaction: inside an open transaction but not running anything, which holds locks and a connection

Setting application_name per process type makes this output far more useful. In Django:

DATABASES["default"]["OPTIONS"] = {
    "application_name": os.environ.get("DB_APPLICATION_NAME", "django-web"),
}

Set DB_APPLICATION_NAME=celery-worker in the worker service's environment.

2. Do the connection math

Django opens one connection per thread that touches the database. For a typical stack:

ComponentFormulaExample
Gunicorn webservers × workers × threads3 × 4 × 2 = 24
Celery workersworker hosts × concurrency2 × 8 = 16
Celery Beat11
Cron or management commandsconcurrent jobs2
Admin, migrations, monitoringreserve10
Total53
add up every connection holderWeb: 3 × 4 workers × 4 threads48Celery: 3 × concurrency 824Cron, shells, admin tools~10Deploy overlap (old + new)+30max_connections1000255075100connectionsSteady state fits; a deploy overlap does notdjango-deployment.com
Managed databases also reserve a few slots for themselves.

With persistent connections (CONN_MAX_AGE > 0), those numbers are the steady-state count, not just the peak. During rolling deploys, old and new processes overlap, briefly doubling the web tier's connections.

If the total exceeds max_connections minus reserved slots, errors are guaranteed under load.

3. Fix idle connection buildup

CONN_MAX_AGE keeps a connection open between requests:

  • 0 (default): close at the end of each request
  • a positive number: reuse for that many seconds
  • None: keep forever
idle connections still countCONN_MAX_AGE = None✕one connection per thread, forever✕idle threads hold slots✕dead ones linger after failoversCONN_MAX_AGE = 60✓reused while busy✓closed after a quiet minute✓CONN_HEALTH_CHECKS onEvery thread in every worker is a potential connectiondjango-deployment.com
Lower thread counts reduce the ceiling too.

Persistent connections reduce connection setup latency, but each worker thread holds its own connection even when idle. With many threads, that adds up quickly. Options:

  • Keep CONN_MAX_AGE modest (for example 60) and reduce threads per worker.
  • Use CONN_MAX_AGE = 0 for components that rarely use the database, such as some Celery queues.
  • Enable CONN_HEALTH_CHECKS = True so broken persistent connections are replaced cleanly instead of causing errors.

Note that with ASGI or async views, Django closes connections at the end of each request regardless, and persistent connections should be disabled; use a pool instead.

4. Fix idle in transaction sessions

SELECT pid, usename, application_name, now() - xact_start AS xact_age, left(query, 100) AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_age DESC;

Causes in Django code:

  • transaction.atomic() blocks that call slow external APIs or send email while the transaction is open
  • ATOMIC_REQUESTS = True combined with slow views, so every request holds a transaction for its whole duration
  • management commands or shells left open in the middle of a transaction
  • Celery tasks that open a transaction and then wait on network calls

Move network calls outside atomic blocks, and use transaction.on_commit() for side effects. Add a server-side safety net:

ALTER ROLE myapp SET idle_in_transaction_session_timeout = '60s';

PostgreSQL terminates sessions that stay idle inside a transaction longer than that, releasing their locks and connections. The application receives an error for that transaction, which is preferable to a stuck database.

5. Terminate stuck connections during an incident

To restore service quickly, you can terminate idle connections from a specific source:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE usename = 'myapp'
  AND state = 'idle'
  AND now() - state_change > interval '5 minutes';
free slots now, carefullySELECTpg_terminate_backend(pid) FROM pg_stat_activityWHERE datname = 'myapp'AND state ='idle in transaction'AND state_change < now() - interval'10 minutes';1231Ends the session; its transaction rolls back.2Target stuck sessions, not active queries.3Only clearly stuck ones.An emergency measure: fix the cause afterwardsdjango-deployment.com
Run a SELECT with the same WHERE first to see what will be ended.

Django reconnects automatically on the next request (with CONN_HEALTH_CHECKS enabled, persistent connections are checked first). This is a stopgap; the connection count will climb again unless you fix the cause.

6. Reduce demand

  • Fewer workers per server, more servers: does not reduce total connections by itself; the formula counts both.
  • Threads versus processes: each thread uses a connection when it queries, so switching from processes to threads does not save connections.
  • Celery concurrency: workers with high concurrency on I/O tasks can hold many connections. Close connections for long-running tasks that do little database work, or use a separate, lower-concurrency queue for database-heavy tasks.
  • Right-size by traffic: many deployments run far more web workers than their traffic needs. Measure utilization before scaling up.

7. Add connection pooling

When real demand exceeds what the database can hold, pool connections:

PgBouncer (external pooler): many client connections share a small pool of server connections in transaction mode. Works for all process types and multiple servers.

many clients, few server connectionsDjango workers and Celeryhundreds of client connectionsPgBouncer, transaction poolingqueues and multiplexesPostgreSQLa fixed pool, e.g. 20 per databaseTransaction pooling breaks session state like server-side cursorsdjango-deployment.com
Set DISABLE_SERVER_SIDE_CURSORS when using transaction pooling.

Django's built-in pool (Django 5.1+ with psycopg 3):

DATABASES["default"]["OPTIONS"] = {
    "pool": {"min_size": 2, "max_size": 4, "timeout": 10},
}
DATABASES["default"]["CONN_MAX_AGE"] = 0

The built-in pool is per process, so total connections become processes × max_size. It requires CONN_MAX_AGE = 0. It helps with connection setup cost and bursts within a process, but it does not multiplex across servers the way PgBouncer does.

8. Raising max_connections carefully

Raising max_connections can be appropriate on a database with enough memory, but each connection uses memory for its process and for work_mem during sorts and hashes. On managed databases, the maximum is often tied to instance size. Prefer pooling over large max_connections values; hundreds of mostly idle connections still cost memory and context switching.

9. Verify

SELECT application_name, state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2
ORDER BY 3 DESC;

Verification check:

  • total connections stay well below max_connections during peak traffic and deploys
  • no sessions remain idle in transaction for longer than a few seconds under normal operation
  • no too many clients errors in application logs
  • load tests at expected peak concurrency do not exhaust connections

Explanation

Django's connection handling is simple: a connection per thread, opened on first use, closed at the end of the request unless CONN_MAX_AGE keeps it. That simplicity means the connection count mirrors your concurrency configuration exactly. Too-many-connections errors are therefore capacity planning problems (the formula exceeds the limit), leak problems (connections or transactions that never end), or both. Pooling changes the formula by letting many application threads share fewer database connections.

Edge cases / notes

Managed databases reserve slots

Services often reserve connections for their own monitoring and replication, so the usable limit is lower than max_connections.

Serverless and autoscaling platforms

Platforms that scale instances rapidly can open hundreds of connections in seconds. Use an external pooler or the provider's pooling endpoint.

Long-running shells

A forgotten manage.py shell or dbshell holds a connection indefinitely. Include tools and humans in your connection budget.

Monitoring connections before they run out

Track connection usage as a percentage of the limit and alert well before 100 percent:

SELECT
  count(*) AS total,
  current_setting('max_connections')::int AS max_connections,
  round(100.0 * count(*) / current_setting('max_connections')::int, 1) AS pct_used,
  count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx
FROM pg_stat_activity
WHERE backend_type = 'client backend';

Alert at around 75 to 80 percent sustained usage and on any long idle in transaction sessions. Most metrics exporters for PostgreSQL expose these values.

Logging connections from Django to spot leaks

In staging, you can see where connections are opened and closed with PostgreSQL's connection logging:

ALTER SYSTEM SET log_connections = on;
ALTER SYSTEM SET log_disconnections = on;
SELECT pg_reload_conf();

Disconnection log lines include session duration. Very long sessions from web processes are expected with persistent connections; from management commands or scripts they usually indicate a process that never exited. Turn the settings off again on busy production servers, because they add log volume.

Celery-specific settings

Celery workers run tasks outside Django's request cycle, so the CONN_MAX_AGE cleanup that happens at the end of each request does not apply automatically in the same way. Django closes stale connections through signals that fire at request boundaries; for workers, close old connections around tasks:

from celery.signals import task_postrun, task_prerun
from django.db import close_old_connections


@task_prerun.connect
def close_before_task(**kwargs):
    close_old_connections()


@task_postrun.connect
def close_after_task(**kwargs):
    close_old_connections()

close_old_connections() closes connections that are unusable or older than CONN_MAX_AGE, which keeps long-running worker processes from holding broken or stale connections indefinitely.

Common mistakes

  • Setting CONN_MAX_AGE = None with many threads and servers.
  • Scaling Gunicorn workers during a traffic spike without checking the database limit.
  • Calling external APIs inside transaction.atomic().
  • Raising max_connections to thousands on a small database instance.

Next steps

FAQ

What causes "sorry, too many clients already" in Django?

The total number of open PostgreSQL connections from all Django processes, Celery workers, and tools reached max_connections. Persistent connections and many threads make this more likely.

How many database connections does Django use?

One per thread that accesses the database. With persistent connections, that is roughly servers × Gunicorn workers × threads for the web tier, plus background workers and tools.

Should I increase max_connections in PostgreSQL?

Only moderately and when the database has memory for it. Pooling with PgBouncer or Django's psycopg pool usually solves the problem more safely.

How do I find which service is using the most Postgres connections?

Query pg_stat_activity grouped by application_name, usename, and client_addr. Setting application_name in Django's database options for each service makes the output clear.

Keep going