Synthetic Industry

Troubleshooting guide · updated 2026-10-11

Timed out waiting for a database connection: pool size, leaks and server limits are different problems

How to tell an undersized pool from leaked connections, from many processes multiplying the pool, from a pooler mode that breaks features, using SQLAlchemy, Django, PostgreSQL and PgBouncer documentation.

One message, five causes

The errors look the same: the pool timed out waiting for a connection, or the server says it has too many connections. But they come from different faults, and the cure for one makes another worse. The pool may simply be smaller than the concurrency the service allows. Connections may be checked out and never returned. Every process may carry its own pool, so the total across processes passes the server's limit. A pooler in front of the server may be in a mode that breaks features the code uses. Or requests may hold a connection while waiting on something slow, such as an outside call.

Raising a limit treats all five the same way and fixes at most one. So begin by counting where connections are and what they are doing.

  • Count connections by state at the busy time: active, idle and idle in a transaction.
  • Count processes and workers across every host, including background workers and schedulers.

The arithmetic: pool times processes

Pools are usually per process. SQLAlchemy's default QueuePool allows 5 persistent connections and 10 overflow, so up to 15 per engine; twenty worker processes with the defaults could ask for 300 connections. PostgreSQL's max_connections is typically 100 unless changed and is fixed at server start, and some of its memory is sized from it. Django adds its own wrinkle: each thread holds its own connection, and the documentation says the database must support at least as many connections as there are threads. Django closes connections at the end of each request by default, and keeps them for a time only if you set CONN_MAX_AGE.

SQLAlchemy's documentation also warns that pooled connections must never be inherited by a forked child process; each child needs a fresh pool, which is a common way for a pre-forking server to end up sharing sockets or multiplying connections unexpectedly.

  • Write the budget as a table: process type, count, pool size, total, against the server limit.
  • Leave headroom for administrators, migrations and monitoring.

Leaks, long holds and idle connections

A leak is a connection taken and not returned: code that opens a session and does not close it on an error path. SQLAlchemy can log checkouts and checkins with echo_pool, and pool_pre_ping and pool_recycle help when the server or a firewall closes idle connections. A long hold is different: the connection is returned eventually, but a request keeps it during a slow outside call, so the pool drains under load. The fix is to release the connection before the slow call, not to enlarge the pool.

Smaller can be better. The HikariCP maintainers argue that a small pool, saturated with waiting threads, can outperform a large one, and that you should size by testing under load rather than by formula. That applies beyond Java: more connections than the database can usefully run in parallel mostly add queueing inside the server.

  • Look for sessions idle in a transaction for long periods.
  • Move slow calls outside the transaction scope.

Poolers have rules, and what the paid job covers

A server-side pooler such as PgBouncer can let many clients share few server connections, but the mode matters. In transaction pooling a client holds a server connection only during a transaction, and PgBouncer's documentation lists features that do not work there, including SET and RESET, LISTEN, WITH HOLD cursors and session-level advisory locks. Test the application against it before relying on it.

The connection-pool outcome diagnoses one service by connection state, writes the budget across all its processes, corrects settings and code, and shows a load test at your stated concurrency before and after. It can trial a pooler on staging. It does not change your database server's limits in production or install a pooler there, and it makes no promise beyond the load level tested.

  • Keep the monitoring queries; they let you repeat the check after each release.
  • Alert on connection count approaching the budget, not just on errors.

Sources and limits

  • SQLAlchemy 2.0: Connection pooling Checked 2026-10-11.
    • QueuePool defaults to a pool size of 5, an overflow of 10 and a 30 second timeout, so at most 15 connections per engine, and pooled connections must not be inherited by a forked child process.
    • pool_pre_ping tests a connection at checkout, pool_recycle replaces older connections, and echo_pool logs checkouts and checkins.
  • Django 5.2: Databases Checked 2026-10-11.
    • CONN_MAX_AGE defaults to 0 so connections close after each request; each thread has its own connection, so the database must support at least as many connections as threads; Django 5.1 adds a native pool option for psycopg 3.
  • PostgreSQL 18: Connection settings Checked 2026-10-11.
    • max_connections is typically 100 by default, can only be set at server start, and some resources are sized from it; superuser_reserved_connections keeps slots for superusers.
  • PgBouncer: Features Checked 2026-10-11.
    • Session, transaction and statement pooling exist; features such as SET, LISTEN, WITH HOLD cursors and session-level advisory locks are not supported in transaction pooling.
  • HikariCP wiki: About pool sizing Checked 2026-10-11.
    • The pool library's maintainers argue that a small pool can outperform a large one and that size should be tuned by testing, treating a formula as a starting point.