The HPA solves one problem (not enough pods for the load) by creating another, rarely anticipated: every additional pod opens its own connection pool to the database, and a Postgres database has a strict, finite limit on simultaneous connections. Under the exact load that triggers the HPA’s scale-up, the database can hit that limit before the HPA has even finished creating the pods meant to absorb it.

The math that doesn’t hold at scale

An application opening a pool of 20 connections per instance, with an HPA configured to scale up to 15 replicas, potentially demands 300 simultaneous connections to the database, a number that far exceeds Postgres’s default max_connections (100 in many configurations). The replica count is precisely what the HPA chooses dynamically based on load, which keeps this ceiling invisible as long as traffic stays moderate, and brutal the day it doesn’t.

-- The hard ceiling that breaks everything past it,
-- often never sized with the HPA in mind
SHOW max_connections;
-- 100

The misleading symptom: errors that look like an application bug

Once the database hits its ceiling, new connections fail with an error (FATAL: too many connections), a message that reads like an application or network bug rather than a sizing problem. The trap compounds in cascade: pods failing to connect can fail their readiness probe, pulling them out of service, reducing effective capacity, and pushing the HPA to create even more replicas, each in turn trying to open connections to an already-saturated database.

PgBouncer: a middleman that multiplexes connections

PgBouncer sits between application pods and Postgres, multiplexing a large number of application connections onto a small number of real connections to the database. In transaction mode (the most common), a Postgres connection is only held for the duration of a transaction, then immediately released for another caller, letting hundreds of pods share a few dozen real connections.

# pgbouncer.ini: 20 real connections to Postgres
# absorb hundreds of application connections
[databases]
myapp = host=postgres port=5432 dbname=myapp

[pgbouncer]
pool_mode = transaction
default_pool_size = 20
max_client_conn = 1000
# Application pods connect to PgBouncer,
# never directly to Postgres
env:
  - name: DATABASE_URL
    value: "postgresql://pgbouncer:6432/myapp"

What transaction mode breaks

PgBouncer’s transaction mode, the most efficient at multiplexing, silently breaks certain features that assume a persistent Postgres session: session-level prepared statements, custom session variables, or LISTEN/NOTIFY. An application using these features unknowingly discovers the problem as errors hard to trace back to PgBouncer, a trade-off to validate explicitly before deploying this mode rather than after the incident.

Takeaway

The HPA scales pods with no knowledge of the connection limit on the database they’re calling, a blind spot that stays invisible as long as load never pushes the replica count toward its configured maximum. PgBouncer solves the problem by multiplexing connections rather than multiplying them, at the cost of real restrictions on certain Postgres features in transaction mode, to validate before deployment. Sizing max_connections around the HPA (not the other way around) is one of the capacity calculations that come up the moment a Kubernetes migration introduces autoscaling in front of a connection-limited database.