idle_in_transaction_session_timeout exists because application code cannot be trusted to finish what it starts. An ORM opens a transaction, an exception takes an unexpected exit path, the connection goes back into the pool with the transaction still open, and PostgreSQL now has a session that will hold everything it touched until someone notices. This parameter is the someone.

The default is 0, meaning disabled. The context is user, the unit is milliseconds, and the maximum is INT_MAX milliseconds, a bit under 25 days, in case your pessimism about the application has a very specific shape.

The mechanics: each time a backend finishes a command and goes back to waiting for the client while a transaction is open, it arms a timer. If the client says nothing before the timer expires, the backend gives up. The transaction is rolled back, the connection is closed, the server logs FATAL: terminating connection due to idle-in-transaction timeout, and the client receives that message (SQLSTATE 25P03) the next time it touches the socket. Note what is being measured: each individual silence, not the transaction. A transaction that issues one statement every four minutes will never trip a five-minute idle timeout, no matter how long it runs. Bounding total transaction time is the job of transaction_timeout, added in PostgreSQL 17. (On 17 and later, if transaction_timeout is set at or below this parameter, the idle timer is never even armed; the transaction timer will get there first.)

What makes an idle transaction poisonous enough to deserve its own executioner: it is a bundle of things other sessions are waiting on. Every table it touched keeps at least ACCESS SHARE until commit or rollback; a session that ran one SELECT and went to sleep is still holding that lock hours later. The classic outage follows directly. A migration’s ALTER TABLE queues behind the sleeper’s ACCESS SHARE, every new query queues behind the ALTER TABLE’s pending ACCESS EXCLUSIVE, and the application is down while pg_stat_activity shows almost nothing running. I have diagnosed this exact pileup in production more than once; the root cause is never the DDL.

Then there is vacuum. If the idle transaction has written anything, or is running at REPEATABLE READ or SERIALIZABLE, it pins the xid horizon (visible as backend_xid or backend_xmin in pg_stat_activity), and no vacuum in that database can remove any tuple that died after the transaction began. Hours of that is bloat everywhere; days of that is a wraparound story. There is one partial exemption: a read-committed transaction that has only read holds no snapshot between statements, so it pins its locks but not the horizon. This is less comforting than it sounds, since leaked transactions have usually written something. A session that is idle in transaction (aborted) has already released its locks and snapshot during error cleanup and merely wastes a connection slot; the timeout kills those too, which is fine. A session that is merely idle, with no transaction at all, holds nothing and is a different parameter’s business (idle_session_timeout, since PostgreSQL 14).

Finding the offenders is one query:

1SELECT pid, usename, now() - state_change AS idle_for, query
2FROM pg_stat_activity
3WHERE state LIKE 'idle in transaction%'
4ORDER BY idle_for DESC;

Killing them automatically is this parameter, and you should. Set it globally to a value no legitimate workload will hit; 5min is a reasonable backstop for an OLTP system, and I have never seen an application that needed an hour of mid-transaction silence, only applications that got it. Because the context is user, you can then tighten it per role for pooled application users (ALTER ROLE app SET idle_in_transaction_session_timeout = '1min') and loosen it for the humans and batch jobs that genuinely think between statements. The same context means any session can SET it back to 0, so understand what you have: a seatbelt for well-intentioned code, not a fence against hostile code. RDS and Aurora ship it at 24 hours instead of PostgreSQL’s 0, which concedes that the problem exists while scheduling the response for a full working day after the damage started.

The application that trips a five-minute idle timeout was broken before the timer fired; the FATAL is just how you find out. A rolled-back transaction and a reconnect is a far better Tuesday than an ALTER TABLE queued behind a session whose owner went home for the weekend.