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.