Summary
In a large production deployment we've measured PgDog's client login path saturating at ~550–700 new client connections/s per instance, while the instance uses only ~0.5 of its 4 cores (workers = 4). Past that rate, TCP accepts succeed but startup/auth lags; clients hit their own connect_timeout mid-startup and PgDog logs and reports nothing (no errors, no warnings), because from its perspective the connection simply went away before login completed. Adding instances fixes it linearly; adding cores to an instance does not — which suggests a serialization point in the login path rather than CPU exhaustion.
Environment
- pgdog v0.1.52,
pooler_mode = "transaction", passthrough_auth = "enabled_plain_allow_change", no client TLS
- 4 vCPU /
workers = 4 per instance, Kubernetes (arm64), listen backlog raised via sysctls (accept-queue overflow was measured and ruled out as the main cause — ~373 overflow drops vs ~130k client-side connect timeouts during the worst event)
- Client fleet: Django/psycopg2, ~180 app instances, a fresh connection per request (no client-side pooling), so a rolling deploy of the app fleet produces reconnect waves of ~7k connects/s fleet-wide
What we observed
- At 8 instances (~600/s/pod capacity each), a fleet-wide reconnect wave (~7k/s) drove client-side connect p99 to a pinned 5s (their libpq
connect_timeout), with tens of thousands of failures over ~30 min — while pgdog.errors stayed 0, CPU stayed ~12% of the pod, and logs were silent.
- Scaling to 32 and then 80 instances made the same waves invisible (~175/s/pod → connect p99 ~20ms flat).
- Separate load testing measured the per-instance wall at ~700 connects/s against a warm instance and ~550/s under sustained load, roughly independent of core count.
Reading the code, candidate suspects
(From frontend/listener.rs / frontend/client/mod.rs / backend/databases.rs on main — happy to be corrected.)
- With passthrough auth, every login calls
databases::add(user). The password-matches path looks cheap (read + constant_time_eq), but the password-differs / first-seen path takes the global config lock, clones the entire config, calls set() and reload_from_existing() — per login. During a credential rotation with allow_change, a reconnect wave would hit this path on every login. We don't believe this is our steady-state wall (our password is stable), but it looks pathological under rotation + storm.
- The login exchange is a sequential multi-await conversation (extra ClearTextPassword round trip under passthrough), so logins-in-flight can pile up — but that alone shouldn't cap throughput at ~600/s on an idle CPU.
- Everything spawned from a single accept task via
comms.tracker().spawn() — accept itself seems unlikely to be the wall at these rates.
Ask
Related: we'll file a separate issue about making login-path saturation observable (today it's invisible in both logs and metrics).
Summary
In a large production deployment we've measured PgDog's client login path saturating at ~550–700 new client connections/s per instance, while the instance uses only ~0.5 of its 4 cores (
workers = 4). Past that rate, TCP accepts succeed but startup/auth lags; clients hit their ownconnect_timeoutmid-startup and PgDog logs and reports nothing (no errors, no warnings), because from its perspective the connection simply went away before login completed. Adding instances fixes it linearly; adding cores to an instance does not — which suggests a serialization point in the login path rather than CPU exhaustion.Environment
pooler_mode = "transaction",passthrough_auth = "enabled_plain_allow_change", no client TLSworkers = 4per instance, Kubernetes (arm64), listen backlog raised via sysctls (accept-queue overflow was measured and ruled out as the main cause — ~373 overflow drops vs ~130k client-side connect timeouts during the worst event)What we observed
connect_timeout), with tens of thousands of failures over ~30 min — whilepgdog.errorsstayed 0, CPU stayed ~12% of the pod, and logs were silent.Reading the code, candidate suspects
(From
frontend/listener.rs/frontend/client/mod.rs/backend/databases.rson main — happy to be corrected.)databases::add(user). The password-matches path looks cheap (read +constant_time_eq), but the password-differs / first-seen path takes the global config lock, clones the entire config, callsset()andreload_from_existing()— per login. During a credential rotation withallow_change, a reconnect wave would hit this path on every login. We don't believe this is our steady-state wall (our password is stable), but it looks pathological under rotation + storm.comms.tracker().spawn()— accept itself seems unlikely to be the wall at these rates.Ask
workers) expected? Is there a known lock/serialization point in the login path?Related: we'll file a separate issue about making login-path saturation observable (today it's invisible in both logs and metrics).