PostgreSQL is a relational database server that applications use to store and query data. It limits how many connections it accepts at once. Applications often keep connections open in a pool so requests can reuse them. Those connections occupy slots even while idle.
A “postgres too many connections” failure can appear during a deployment even when the normal replica count looks safe. A replacement instance may open its pools before the older instance closes its connections. If each worker process owns a separate pool, one additional instance can add several pools at once.
The result is failed connection attempts, which can interrupt requests or jobs. PostgreSQL reserves some slots for privileged access, so ordinary applications can be refused before all slots are occupied. A proxy or external pooler changes this accounting: application connections to the proxy are not necessarily one-to-one with PostgreSQL sessions. PostgreSQL connection settings
Bounded application pools help control demand. An external pooler such as PgBouncer can also let many application connections share fewer PostgreSQL sessions. Neither removes the need to budget connections during deployments. An error alone does not establish which clients occupied the slots or prove a connection leak. Combine the actual refusal with database activity snapshots, pool settings and records showing when old and new instances opened or closed connections.
Diagnose connection exhaustion during a deployment
These checks apply to application operators investigating connection failures around a release. They help identify the rejecting limit, account for sessions and verify that fresh connections recover. They do not reconstruct past connection ownership if no snapshot or lifecycle evidence was retained.
Identify the rejecting limit
Preserve the error message and SQLSTATE when the driver exposes it. PostgreSQL assigns 53300 to too_many_connections. A message such as sorry, too many clients already describes server admission failure. A client pool acquisition timeout can instead mean that all connections in that process's pool are busy; it does not, by itself, establish server exhaustion. PostgreSQL error codes
Use an existing authorized administrative connection where possible. Repeatedly opening diagnostic sessions adds demand to an already constrained server. These read-only queries inspect the connected server:
SELECT current_setting('server_version') AS server_version;
SELECT name, setting, unit, pending_restart
FROM pg_settings
WHERE name IN (
'max_connections',
'superuser_reserved_connections',
'reserved_connections'
)
ORDER BY name;
The examples use PostgreSQL 18 documentation. Check the documentation for your installed major version; an older version may not expose every setting above. A missing row is not evidence of a zero reserve.
Check the application login role and destination database too. Substitute their actual names; current_user in an administrative session would inspect the administrator instead.
SELECT rolname, rolconnlimit
FROM pg_roles
WHERE rolname = 'checkout_app';
SELECT datname, datconnlimit
FROM pg_database
WHERE datname = 'checkout';
A value of -1 means no configured connection limit at that level. Role and database limits are separate from overall server capacity. Their enforcement has documented exceptions and is approximate; do not treat them as exact capacity counters. Role connection limits, database connection limits
Count sessions and overlapping pools
Capture the snapshot while failures are happening. This query avoids SQL text and groups client connections by their reported identity:
SELECT datname,
usename,
application_name,
state,
count(*) AS connections
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY datname, usename, application_name, state
ORDER BY connections DESC, application_name;
Run repeated samples as separate statements in autocommit mode. Repeated reads inside one transaction can reuse activity information. The observer's own client connection is included. Ordinary roles have restricted visibility into other sessions, so request the necessary monitoring access rather than interpreting hidden details as absent activity. PostgreSQL activity monitoring
Set a useful application_name through your driver's supported configuration. For example, distinguish checkout-old-a-worker-1 from checkout-new-a-worker-1. It is a client-supplied label, not authenticated proof of ownership. Confirm it against deployment records.
Now inventory every pool in each process. Include web workers, background workers, scheduled jobs, migrations and other services using the same database server. A configured pool maximum describes potential demand; a lazy pool may currently hold fewer connections.
Consider this illustrative budget, not a measured run:
| Item | Potential connections |
|---|---|
| Two old instances × two processes × four connections | 16 |
| One replacement instance × two processes × four connections | 8 |
| Total during overlap | 24 |
With max_connections = 20, three superuser-reserved slots and reserved_connections = 0, ordinary roles have a ceiling of 17 slots. The old instances can fit at 16, but adding a replacement creates potential demand for 24. Other ordinary clients reduce the remaining room further. This arithmetic assumes direct connections and application roles without reserved-slot privileges.
For Kubernetes, count more than the desired replicas. With replicas: 3 and maxSurge: 25%, the percentage rounds upward to one surge pod. Terminating pods can remain alive and consume resources beyond replicas plus surge. Count their database connections separately until they close. Kubernetes rolling deployments
An illustrative rollout budget with two processes per pod and four connections per process is:
3 desired pods + 1 surge pod = 4 non-terminating pods
4 × 2 × 4 = 32 potential connections
2 additional terminating pods × 2 × 4 = 16
rollout application demand = 48
The two terminating pods are an explicit example assumption, not a Kubernetes guarantee. Add migrations and other clients, then leave operational headroom. Repeat the calculation for autoscaling and rollback. Reducing surge may reduce demand, but it does not close pools in terminating processes.
Use bounded application pools
If the application opens a new connection for each operation, use the driver's or framework's supported connection pool to reuse connections. If it already pools connections, check that requests share the intended pool rather than creating another pool for every request.
Set an explicit maximum for each pool and budget all copies of it. Also check idle-connection limits and how long callers may wait to acquire a connection. For example, Go's sql.DB already manages a pool: SetMaxOpenConns limits open connections, while SetMaxIdleConns controls retained idle connections. Go documents that operations wait when the open-connection limit is reached. Go connection management
For the earlier three-instance overlap, there are six process-local pools. An illustrative maximum of two connections per pool gives a potential application demand of 12 instead of 24. That leaves five of the 17 ordinary slots for other clients and headroom. It does not establish that two connections per process can serve the workload. Test request latency, acquisition waits and job throughput before adopting that limit.
Return borrowed connections promptly, close result sets as required by the driver, and finish transactions on every error path. A pool cannot reuse a connection that application code still holds. A smaller pool may protect PostgreSQL while increasing waits inside the application, so check both outcomes.
Consider an external pooler such as PgBouncer
An external pooler sits between applications and PostgreSQL. It can be useful when many application processes each keep a small pool, especially when those processes overlap during releases.
PgBouncer offers different pooling modes. In session mode, a client keeps its PostgreSQL connection for the client's entire session. Long-lived application pools can therefore continue to occupy server connections. In transaction mode, PgBouncer returns the server connection to its pool after each transaction, allowing another client to use it. Long-running transactions still hold their assigned connections. PgBouncer pooling modes
Check compatibility before choosing transaction mode. Session-level advisory locks and LISTEN, for example, are incompatible with that mode. Prepared-statement support depends on how statements are prepared and on configuration; check the documented feature table against your driver and ORM. Test normal requests, migrations and background jobs before changing the production connection endpoint. PgBouncer feature compatibility
Budget the pooler's PostgreSQL connections separately from its application connections. PgBouncer's max_client_conn limits incoming clients. default_pool_size limits server connections per user/database pair, rather than across the whole installation. Additional pairs and reserve pools can increase demand. max_db_connections and max_user_connections provide further server-connection limits. PgBouncer configuration
Add the backend budgets of every pooler instance, including old and new poolers during their own rollout. Include applications that bypass the pooler. A pooler beside every application pod does not create one shared connection ceiling.
If a managed database offers a pooling endpoint, verify its mode, limits and session-feature support in that provider's documentation. Whether self-managed or hosted, pooling can move excess demand into a queue; it cannot make slow transactions finish faster. Keep application waits bounded and monitor both client waits and PostgreSQL sessions.
Verify draining and recovery
First contain additional demand using the application's established release controls. Avoid repeatedly starting replacement workers while existing pools remain open. Choose a pool or rollout change from the measured ownership and the capacity budget.
On shutdown, stop accepting new work, let in-flight work finish within a bounded period and close each process's pools. Verify this behavior in the application; removing a pod from traffic does not itself demonstrate that its database sessions have closed.
Take another activity snapshot. For direct connections, confirm that the old instance labels disappear. With an external pooler, also inspect its client and server connection records: PostgreSQL sessions may remain open for reuse after an application disconnects. Verify that replacement instances can establish fresh application connections through the intended path. Then verify real requests and jobs recover. An already-open session successfully running a query does not test admission of a new connection.
Do not terminate all idle sessions indiscriminately. Idle connections can be legitimate pool members, and their owners may immediately reconnect. If a specific session must be ended, establish its owner and consequences first. Likewise, increasing max_connections requires resource planning and a server restart; it is not a universal fix.
For a staging test, use a non-superuser application role and distinct old/new application names. Record the PostgreSQL version and reserves, fill the old pools, introduce replacement pools, capture the refusal and SQLSTATE, then close the old pools and test a fresh connection. Save snapshots at each stage. If introducing a pooler, repeat the test through it and record both client and backend counts. Expected arithmetic is not a substitute for observed results.
Keep a deployment timeline without logging secrets
Record connection failures separately from pool lifecycle events. A pool object being created does not necessarily mean it has opened database sessions. Record actual open counts when your library exposes them, and distinguish a drain request from a completed pool close.
This synthetic event illustrates safe correlation fields; it is not output from a reproduced failure:
{
"timestamp": "2026-11-11T18:15:00Z",
"severity": "ERROR",
"service_name": "checkout-api",
"host": "checkout-new-a",
"message": "Synthetic connection refusal; marker=pg-overlap-demo-01",
"instance_id": "checkout-new-a-worker-1",
"deployment_revision": "release-b",
"event": "db_connection_rejected",
"sqlstate": "53300"
}
Also emit events such as pool_drain_started and pool_closed, with the same instance and revision identifiers. Exclude passwords, connection strings, authorization headers and SQL parameters. Protect retained logs with appropriate access and retention settings. The microservices logging guide explains how consistent service and instance context helps connect these records.
Inspect the deployment window in Fluxtail
Fluxtail can retain sanitized application events for comparison across old and new instances. Start with one synthetic marker to verify delivery before relying on the collection path during an incident.
Save the JSON above as connection-event.json. In an interactive Bash shell, use curl 7.76 or newer and replace the URL with the exact HTTPS URL of your HTTP JSON receiver. The token must be bound to that receiver. This sends a synthetic test event; it does not test PostgreSQL connectivity.
receiver_url='https://ingest.fluxtail.io/v1/receivers/RECEIVER_ID/logs'
read -r -s -p 'Receiver token: ' ingest_token
printf '\n'
printf 'Authorization: Bearer %s\n' "$ingest_token" |
curl -q --silent --show-error --fail-with-body \
--max-time 15 --request POST \
--header @- --header 'Content-Type: application/json' \
--data-binary @connection-event.json \
--write-out '\nHTTP %{http_code}\n' "$receiver_url"
unset ingest_token
The token travels through standard input rather than curl's command arguments. Keep shell tracing disabled. A single accepted event returns HTTP 202 with {"accepted":1}; acceptance alone does not prove storage or searchability. Find pg-overlap-demo-01 in the receiver's stream in Live Tail. Use a fresh marker and timestamp for later tests. The data ingestion example covers receiver routing and delivery checks.
For the actual deployment, narrow the time window around the first refusal. Compare the recorded instance identifiers and revisions in raw events, checking which field mappings your receiver preserves before relying on field filters. Pair those events with the contemporaneous database snapshots and, if applicable, pooler records. Failure logs establish when clients were refused; snapshots and pool settings establish which sessions occupied capacity.
Use the log-reading guide for bounded investigation and centralized log management for collecting records across application instances. Verify recovery with fresh application connections and successful requests or jobs. Quiet error logs alone do not establish recovery.