Good rundown, and the PgBouncer transaction-mode tradeoff is the part people usually skip over. One thing worth adding to the "ORMs that don't clean up" cause: even with an in-app pool in place, a request that opens a transaction and then does slow I/O before committing (an external API call, a retry loop, a queue publish) still holds a real Postgres backend the whole time, and PgBouncer in transaction mode can't reclaim it either since it's mid-transaction from its point of view. We rely on idle_in_transaction_session_timeout on the Postgres side to kill those automatically rather than trusting every code path to clean up on its own, and pg_stat_activity filtered on state = 'idle in transaction' is usually the fastest way to find which endpoint is actually the culprit before guessing at pool sizes.
M
Mihai_LeanZero
Atlassian consultant and Forge developer at LeanZero. We do Data Center to Cloud migrations and build Forge apps, and I write up what I learn at leanzero.net.
Great addition, and a sneaky one, because the pool looks healthy while one slow endpoint holds a backend hostage. Agree on setting idle_in_transaction_session_timeout as the backstop. The real fix is usually moving the API call or queue publish outside the transaction, but the timeout catches the paths nobody remembers. Your pg_stat_activity query is the first thing I'd run too.
Good rundown, and the PgBouncer transaction-mode tradeoff is the part people usually skip over. One thing worth adding to the "ORMs that don't clean up" cause: even with an in-app pool in place, a request that opens a transaction and then does slow I/O before committing (an external API call, a retry loop, a queue publish) still holds a real Postgres backend the whole time, and PgBouncer in transaction mode can't reclaim it either since it's mid-transaction from its point of view. We rely on idle_in_transaction_session_timeout on the Postgres side to kill those automatically rather than trusting every code path to clean up on its own, and pg_stat_activity filtered on state = 'idle in transaction' is usually the fastest way to find which endpoint is actually the culprit before guessing at pool sizes.