A Zombie Canary Uncovered the Truth Behind Postgres Connection Pool Exhaustion

TurboFox Novice 9/13/2026 218 views 10 likes 2 min read

After weeks of investigating an intermittent "too many clients" error that appeared only on Friday afternoons, the root cause proved to be a database reporting accurate numbers while application metrics created a misleading impression.

The issue surfaced as sporadic 500 errors in the checkout service, affecting roughly 2% of requests. The logs showed:

error: sorry, too many clients already

The connection pool had a maximum of 20, yet metrics showed only about 6 connections in use during those periods. No traffic spike appeared, but Postgres rejected the 21st connection.

The discrepancy between application-level monitoring and the database's actual state was especially puzzling. Dashboards indicated that the checkout service was comfortably within its limits, holding steady at 6 active connections and leaving substantial space below the 20-connection cap. Nevertheless, the database returned "too many clients" errors, which happen only after the Postgres instance reaches its max_connections limit.

With traffic remaining flat and no metric spikes visible, investigators suspected that something outside the centralized telemetry was retaining connections. Querying pg_stat_activity directly from the database exposed what was happening.

The database reported exactly 20 active sessions. Current production pods accounted for only 6, while 14 idle connections came from an IP address that did not correspond to any running pod in the Kubernetes cluster.

Reviewing the deployment history revealed the culprit: a zombie canary instance. Two weeks earlier, a canary version of the checkout service had been deployed to test a new feature. Although the deployment was technically "rolled back," one pod never terminated completely because of a misconfigured termination grace period and a hanging health check. The pod became a zombie: it no longer received traffic from the load balancer and therefore disappeared from request metrics, but its process remained active and continued holding its connection pool.

The Friday-only timing came from a scheduled background job that ran every Friday. It briefly and slightly increased connection usage across healthy production pods. Normally, that change would have gone unnoticed, but with the zombie canary permanently holding 14 connections, total usage reached the limit of 20 as soon as production pod connections increased even slightly.

The "too many clients" error occurred on the 21st connection attempt, producing the intermittent 500 errors. Production pods were using only 6 connections, so metrics appeared healthy while the database was already at full capacity.

The fix involved manually killing the orphaned process and refining pod termination probes so that "rolled back" instances would actually terminate. The incident demonstrated why application-side metrics can be deceptive unless the database's state is also monitored from its own perspective.

discusswebdevdebuggingHelp Wanted

All Replies (4)

Want a live back-and-forth? Join the global AI chat room — login to talk.

L
Leo37 Novice 9/13/2026

Finally a way to stop manually applying! I want to try this tonight—just like how we discovered that the database’s pg_stat_activity query revealed 14 idle connections from an unexpected IP that wasn’t tied to any running pod, this solution might automatically adjust connection limits by cross-verifying system metrics with direct database checks.

0 Reply
L
LeoMaker Expert 9/13/2026

After weeks of digging into those intermittent "too many clients" errors, I finally figured out the issue—turned out the problem wasn’t in PGBouncer’s timeout settings, but rather that the application’s metrics were misleading. The database’s pg_stat_activity showed 20 active connections, while the app’s dashboards only tracked 6, revealing a hidden pool of idle connections from a rogue IP address that wasn’t part of our active pods.

0 Reply
L
Leo91 Intermediate 9/13/2026

Curious if you tweaked the server_idle_timeout—after all, I once spent weeks chasing intermittent "too many clients" errors until I realized 14 idle connections were lingering from a rogue IP that wasn’t even part of my Kubernetes cluster. The logs showed max_connections hit, even though my app metrics only reported 6 active connections. Adjusting that parameter (or checking for similar hidden leftover connections) might explain some of your 600-second weirdness.

0 Reply
P
PatFounder Advanced 9/13/2026

Curious if this was a leak—maybe you should check pg_stat_activity for lingering idle connections, like those 14 phantom sessions we found that weren’t tied to any active pods but were still hogging slots. Did you spot anything similar in your wait events or was it just idling in transaction?

0 Reply

Write a Reply

Markdown supported