PostgreSQL
PostgreSQL — FATAL: sorry, too many clients already
Written and reviewed by Sahil Srivastav
org.postgresql.util.PSQLException: FATAL: sorry, too many clients already
at org.postgresql.core.v3.ConnectionFactoryImpl.doAuthentication(ConnectionFactoryImpl.java:660)What this error actually means
PostgreSQL itself refused the connection. The server has a hard `max_connections` ceiling, and every backend is a separate OS process with its own memory, so the limit exists to protect the database rather than to annoy you. When the count is reached, new connection attempts are rejected at authentication time — which is why this arrives as a `FATAL` before any query runs.
The important reframe: this is almost never caused by too many *users*. It is caused by too many *pools*. Ten service instances each configured with a pool of 50 want 500 connections from a database whose default `max_connections` is 100. Nobody made that decision explicitly; it emerged from autoscaling a service whose pool was sized for a single instance.
Note that a handful of slots are reserved for superusers (`superuser_reserved_connections`), which is why you can often still connect with `psql` as an admin while the application cannot connect at all. That is your diagnostic window — use it.
Causes, most common first
- 1Pool size multiplied by instance count exceeds max_connections. The dominant cause. Pool sizing is a per-instance setting with a global consequence. The arithmetic that matters is `instances × maxPoolSize + migrations + admin + BI tools`, and it must stay comfortably under `max_connections`.
- 2Connection leaks in one or more services. A service holding connections it never returns steadily consumes global slots. Look for the service whose connection count grows monotonically rather than tracking traffic — the leak is there, and it starves everyone else.
- 3Sessions parked in idle in transaction. These occupy a backend and hold snapshots and possibly locks while doing nothing. From the database’s perspective they are fully-fledged clients. From yours they are invisible, which is why they are so often the missing connections.
- 4Ad-hoc and analytical clients with no discipline. BI tools, notebooks, serverless functions, and laptop `psql` sessions. Serverless is a particular hazard: each concurrent invocation may open its own connection with no pooling at all, so connection count scales with request concurrency.
- 5max_connections genuinely too low for the topology. Real on small managed instances, where the limit is tied to the instance class and can be surprisingly low. Confirm the arithmetic before concluding this — raising the limit costs memory per backend and can trade a clear error for a slow, thrashing database.
When you see it
- Every instance starts failing at once, because they all compete for the same global ceiling
- Failures begin right after a scale-up, a deploy that added replicas, or a new service joining the database
- Admin `psql` still connects while the application cannot, thanks to reserved superuser slots
- Connection count sits exactly at `max_connections` and never drops
- Migrations, cron jobs, and BI tools fail first because they connect last
How to diagnose it
Step 1
See who holds the connections, grouped by client
This is the first query to run. Grouping by application name and state tells you which service and which state — active, idle, idle in transaction — is consuming the ceiling.
SELECT application_name, client_addr, state, count(*)
FROM pg_stat_activity
GROUP BY 1, 2, 3
ORDER BY 4 DESC;Step 2
Compare usage against the actual limit
Establish the headroom in numbers, including the reserved superuser slots that the application cannot use.
SELECT (SELECT count(*) FROM pg_stat_activity) AS in_use,
current_setting('max_connections') AS max_conn,
current_setting('superuser_reserved_connections') AS reserved;Step 3
Find the oldest idle-in-transaction sessions
Long-lived idle transactions are both a connection leak and a vacuum blocker. Anything older than a few seconds here is an application bug.
SELECT pid, application_name, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start
LIMIT 20;Step 4
Do the capacity arithmetic explicitly
Write down instances × pool size for every service that touches this database, then add migrations, cron, admin, and BI. If the total exceeds `max_connections`, you have found the bug regardless of what else is true.
The fix
Set `application_name` on every connection string first. Without it, `pg_stat_activity` shows anonymous clients and you cannot attribute the ceiling to a service. This is a one-line change that pays for itself during the first incident.
Shrink pools rather than growing the limit. A pool of 10 per instance across 10 instances is 100 connections that PostgreSQL can service efficiently; a pool of 50 each is 500 that it cannot. Throughput on a database is bounded by cores and disk, not by how many clients are waiting, so oversized pools add queueing and context switching without adding capacity.
Put a pooler in front for high instance counts or serverless. PgBouncer in transaction mode multiplexes thousands of client connections onto a small set of server connections, which is the only real answer when instance count is elastic. Be aware of the constraint it imposes: transaction-mode pooling breaks session-scoped state — prepared statements, advisory locks held across statements, `SET` that must persist, `LISTEN`/`NOTIFY`.
Fix the leaks and the open transactions, since both consume slots invisibly. Set `idle_in_transaction_session_timeout` so the database defends itself against application bugs rather than waiting for them to be fixed.
Raise `max_connections` only after the arithmetic justifies it, and remember each backend costs memory — raising it on a small instance can convert connection refusals into swapping, which is a worse outage.
# Defend the database from application lifecycle bugs
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
ALTER SYSTEM SET statement_timeout = '30s';
SELECT pg_reload_conf();
# Make every client attributable in pg_stat_activity
jdbc:postgresql://host:5432/app?ApplicationName=orders-apiHow to stop it coming back
- Track connection count as a fraction of `max_connections` and alarm at 70%, not at failure
- Treat pool size as a global budget: document instances × pool for every service against one ceiling
- Always set `application_name`; anonymous connections make attribution impossible mid-incident
- Keep `idle_in_transaction_session_timeout` and `statement_timeout` set in production as standing protection
- Route serverless and BI workloads through a pooler rather than connecting directly
FAQ
Is this the same as HikariCP "connection is not available"?
No, and the difference tells you where to look. The Hikari message means your application pool ran out of its own slots and gave up waiting. This message means PostgreSQL refused a new connection entirely. You can hit the Hikari timeout with the database almost idle, and you can hit this one with every pool locally healthy.
Why not just raise max_connections?
Each backend is a process with its own memory, and work memory is allocated per operation per backend. Raising the limit on a modest instance trades a clean refusal for memory pressure and scheduler thrash — a slow, hard-to-diagnose database instead of a clear error. Reduce demand first.
Does PgBouncer have downsides?
Transaction-mode pooling gives up session state: server-side prepared statements, advisory locks spanning statements, session `SET` values, temp tables, and `LISTEN`/`NOTIFY` all break or behave unexpectedly. Most applications are fine, but verify your driver’s prepared-statement behaviour before rolling it out.
Why do migrations fail first?
They connect last and briefly, so they lose the race for the final slots. It is a useful early warning: migration failures under load usually mean you are already close to the ceiling during normal operation.
Related
Other errors engineers hit next to this one
- QueuePool limit of size 5 overflow 10 reached, connection timed out
- DetachedInstanceError: instance is not bound to a Session
- RuntimeError: Event loop is closed
- Task was destroyed but it is pending!
- Executing <Handle ...> took 2.418 seconds (blocked event loop)
- SettingWithCopyWarning: A value is trying to be set on a copy of a slice
- celery.exceptions.WorkerLostError: Worker exited prematurely
- requests.exceptions.ReadTimeout: HTTPSConnectionPool read timed out