Postgres Reserved Connection Slots: Why 97 Counts as Full
The app is throwing connection errors. You SSH in, open psql, and it works fine. You check max_connections: 100. You count pg_stat_activity: 97. And you sit there for a moment wondering why 97 out of 100 counts as full.
It counts as full because your application never had 100 connections available. It had 97. Postgres holds back a handful of slots so that a superuser can still get in when everything else is jammed, and that reserve is subtracted from max_connections, not added on top of it. Nobody tells you this until you are three connections short at 3am.
The reason this deserves its own article is the second-order failure: you were 4 connections short, so you raised max_connections from 100 to 104, restarted, and it broke again. Because 104 minus the reserve is 101, and your pool wanted 104.
the connections your application can actually use is
max_connections - superuser_reserved_connections - reserved_connections
With stock settings that is 100 - 3 - 0 = 97. Add up the pool maximum of every process that connects to this database (each app instance, cron workers, migrations, the BI tool, your own psql) and keep the total below that number. Raising max_connections is the last resort, not the first, because every slot costs memory whether it is used or not.
The exact errors, and why they are not the same error
On PostgreSQL 15 and earlier you get this:
FATAL: remaining connection slots are reserved for non-replication superuser connections
On PostgreSQL 16 and later the wording changed, and there are now two variants:
FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute
FATAL: remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role
And this is the other one, which looks like the same problem and is not:
FATAL: sorry, too many clients already
All of them carry SQLSTATE 53300 (too_many_connections), which is exactly why they get conflated in every forum thread and in most application error handlers. They are raised in different places by different code, and the fix is different.
| Message | Raised by | What it means |
|---|---|---|
sorry, too many clients already | The postmaster, before authentication | There is no backend slot at all. The server is genuinely full. |
remaining connection slots are reserved ... | Backend startup, after authentication | Slots exist, but they are inside the reserve and your role is not entitled to them. |
The second one is a better error than the first, because it tells you that the server still had capacity. Your role just was not allowed to consume the last of it.
Why this happens
Two GUCs carve a reserve out of the top of max_connections:
superuser_reserved_connections, default 3. Slots only superusers can take. This is the emergency door: if a runaway client exhausts the pool, a DBA can still connect and terminate it.reserved_connections, default 0, added in PostgreSQL 16. Slots reserved for roles that have been granted thepg_use_reserved_connectionspredefined role. This exists so monitoring and operational tooling can get in without being a superuser.
When a regular backend starts, Postgres checks whether the number of free process slots is below superuser_reserved_connections + reserved_connections. If it is, and the connecting role is neither a superuser nor a member of pg_use_reserved_connections, the connection is refused with the message above. Which of the two PostgreSQL 16 messages you get depends on how deep into the reserve you are: below the superuser reserve you are told only superusers may connect; between the two thresholds you are told the pg_use_reserved_connections role is required.
One thing that is not eating your pool: autovacuum workers and background workers. They have their own pools of process slots and the reserve check is not applied to them.
The fix, step by step
Step 1: Find out what is actually holding the slots
SELECT usename,
application_name,
state,
count(*) AS conns,
max(now() - state_change) AS longest_in_state
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2, 3
ORDER BY conns DESC;
You are looking for two things. First, whose connections these are: if one application_name holds 80 of them, you have found your problem without any further arithmetic. Second, the state column. A pile of rows in idle in transaction with a longest_in_state measured in minutes is a connection leak, not a capacity problem, and no amount of raising max_connections will fix it.
Step 2: Do the arithmetic honestly
SELECT current_setting('max_connections')::int AS max_conn,
current_setting('superuser_reserved_connections')::int AS su_reserved,
current_setting('max_connections')::int
- current_setting('superuser_reserved_connections')::int AS usable_pre_16;
On PostgreSQL 16 and later, also subtract current_setting('reserved_connections').
Now list every process that opens a connection and its configured pool maximum. Not the average, the maximum, because that is what you will hit during an incident when everything retries at once. Four app containers with a pool of 20 each is 80, plus a Celery worker set at 10 is 90, plus a scheduled report job at 5 is 95, plus one open psql is 96, and you are one bad deploy away from the error. This sum is the number that matters, and almost nobody has written it down.
Step 3: Fix the pool before you touch the server
In most cases the right change is on the client side. A web app does not need 20 connections per instance; a connection is only useful while a query is running, and the rest of the time it is a reservation against a scarce global resource. Set each instance's pool so that the total across all instances leaves headroom.
If you genuinely have many short-lived clients (serverless functions, a lot of small containers), put PgBouncer in front in transaction pooling mode. That turns hundreds of client connections into a few dozen server connections. Be aware of the constraint that comes with transaction mode: session-scoped features such as LISTEN/NOTIFY, session-level advisory locks and some prepared statement handling do not survive it, so check your driver's settings before switching.
Step 4: Give operations a way in that is not the superuser reserve
The superuser reserve is meant to be a last resort, not the path your monitoring agent uses every 30 seconds. On PostgreSQL 16 and later, set aside slots for tooling explicitly:
ALTER SYSTEM SET reserved_connections = 3;
GRANT pg_use_reserved_connections TO monitoring;
SELECT pg_reload_conf();
reserved_connections requires a restart to take effect because it affects shared memory sizing; the GRANT does not. On PostgreSQL 15 and earlier there is no equivalent, so the practical approach is to keep the monitoring role's connection count small and fixed, and to make sure it is not sharing a pool with the application.
Step 5: Only now consider raising max_connections
ALTER SYSTEM SET max_connections = 200;
-- requires a restart
Two warnings. Every connection is a separate backend process with its own memory, and per-operation memory such as work_mem is allocated per sort or hash node, so a high connection count multiplied by a generous work_mem is a well-trodden route to the OOM killer removing your database at the worst possible moment. And if you raise the limit, raise it to a round number with headroom, not to exactly the count you were short by, or you will be back here next month.
Verification
1. Confirm the usable number, not the configured one.
SELECT (SELECT count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend') AS in_use,
current_setting('max_connections')::int
- current_setting('superuser_reserved_connections')::int AS usable;
in_use should sit comfortably below usable at peak, not brush against it.
2. Prove the app role can still connect under load. During your busiest window, connect explicitly as the application role rather than as postgres:
psql "postgresql://app_user@db-host/appdb" -c "select 1"
Connecting as a superuser proves nothing here, because a superuser can always take the reserve. That is the exact trap that makes this look intermittent: the DBA can always get in, so the report of "the database is refusing connections" looks unreproducible.
3. Watch for the leak coming back.
SELECT count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_txn,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE state = 'active') AS active
FROM pg_stat_activity WHERE backend_type = 'client backend';
If idle_in_txn is anything but near zero, set idle_in_transaction_session_timeout so a forgotten transaction gets cleaned up rather than holding a slot and a lock indefinitely.
What people get wrong
| The advice | Why it is wrong |
|---|---|
Set superuser_reserved_connections = 0 to "get those 3 back" | You have removed your own emergency door. The next time the pool is exhausted you cannot connect to fix it, and the recovery becomes a restart of the database instead of a single pg_terminate_backend. |
Raise max_connections by exactly the shortfall | The reserve is subtracted from the new value too. You will land in the same place. |
| Treat it as identical to "sorry, too many clients already" | They are raised by different code at different stages. One means full, the other means full for you specifically. |
Run pg_terminate_backend on everything idle, on a schedule | This hides a connection leak and will eventually kill something mid-transaction. Find out why connections are not being returned to the pool. |
Set max_connections to 1000 and move on | Postgres is process-per-connection. A thousand backends is a memory and context-switching problem long before it is a throughput solution. Use a pooler. |
When it is still broken
- Connections are idle, not active. Check
state = 'idle'with a very oldstate_change. That is a pool that opens connections and never closes or recycles them, often a framework default of unlimited pool size. - PgBouncer is running in session mode. In session mode a server connection is held for the whole client session, which gives you almost none of the multiplexing you installed it for. Check
pool_modeanddefault_pool_size. - Something is holding a transaction open across a slow external call. An HTTP request inside a database transaction is the classic version. The connection is pinned for the entire round trip. Finding the offending statement is the same discipline as the slow query hunt on the MariaDB side: get the actual statement text first, argue about the fix second.
- Your managed provider has a lower effective limit than
max_connectionssuggests. Some managed Postgres services reserve additional connections for their own agents and backups. Query the setting on the actual instance rather than trusting the plan's advertised number.
The underlying lesson is smaller than the outage it causes: max_connections is not the number of connections your application can make. Write the real number down, put it next to the sum of your pool sizes, and this error stops being a surprise.
Frequently asked questions
- What does 'remaining connection slots are reserved for non-replication superuser connections' mean?
- It means Postgres still has free connection slots but they fall inside the reserve set by superuser_reserved_connections, which defaults to 3, and your role is not a superuser. The usable pool for ordinary roles is max_connections minus superuser_reserved_connections (minus reserved_connections on PostgreSQL 16 and later). On PostgreSQL 16 the wording changed to 'reserved for roles with the SUPERUSER attribute'.
- How is this different from 'sorry, too many clients already'?
- They share SQLSTATE 53300 but come from different places. 'sorry, too many clients already' is raised by the postmaster before authentication and means there is no backend slot at all. The reserved-slots message is raised after authentication and means slots exist but your role is not entitled to the last few. The first needs more capacity or a pooler; the second means you are within a few connections of the ceiling.
- Should I set superuser_reserved_connections to 0 to get more connections?
- No. Those slots are the emergency door that lets a DBA connect and terminate a runaway client when the pool is exhausted. Setting it to zero means your recovery option during the next incident is restarting the database rather than running pg_terminate_backend. If you need slots for monitoring tools specifically, PostgreSQL 16 added reserved_connections and the pg_use_reserved_connections role for exactly that.
- How many connections should my application pool be set to?
- Add up the pool maximum of every process that connects to the database, including each application instance, background workers, cron jobs, migration runners, BI tools and interactive psql sessions, and keep that total below max_connections minus the reserved slots. If you have many short-lived clients such as serverless functions, put PgBouncer in transaction pooling mode in front rather than raising max_connections, since Postgres runs one operating system process per connection.