PostgreSQLIV · Connections, Sessions and PoolingConnections
Idle in transaction, and what it actually holds
What you'll learn
- Distinguish idle from idle in transaction from idle in transaction (aborted)
- Determine from pg_stat_activity whether a session holds a snapshot, an xid, both or neither
- Predict which idle-in-transaction sessions bound the cleanup horizon
- Apply idle_in_transaction_session_timeout deliberately rather than as a blanket
Prerequisites
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
idle in transaction is the state most likely to be named as a root
cause and least likely to be understood precisely. The usual statement
is that such a session blocks VACUUM from reclaiming dead rows. That
is often true in effect and wrong in mechanism, and the difference
decides whether an investigation succeeds.
The states, and what distinguishes them
state | The session is | Server is waiting on |
|---|---|---|
active | Executing a statement | Whatever wait_event says |
idle | Connected, no transaction open | The client, between transactions |
idle in transaction | Inside BEGIN, no statement running | The client, mid-transaction |
idle in transaction (aborted) | Same, after an error, awaiting ROLLBACK | The client |
The wait event for the two idle-in-transaction states is the giveaway:
$ psql -U postgres -c 'SELECT pid, state, wait_event_type, wait_event, now()-xact_start AS xact_age, left(query,40) AS last_query FROM pg_stat_activity WHERE backend_type = ...' pid | state | wait_event_type | wait_event | xact_age | last_query
-----+---------------------+-----------------+------------+-----------------+-----------------------------
924 | idle in transaction | Client | ClientRead | 00:00:04.032709 | CREATE TEMP TABLE t(i int);
(1 row)Client:ClientRead says the database has done its work and is waiting
for the application. The query column shows the last statement
executed, not a running one — a distinction worth internalising,
because it makes the column look like an active query when it is
history.
What three identical-looking sessions actually hold
Three sessions were opened on a live PostgreSQL 18.6 cluster. All three
reach idle in transaction. They differ only in isolation level and in
whether they wrote.
$ psql -U postgres -d bank -c 'SELECT pid, state, backend_xid AS holds_own_xid, backend_xmin AS holds_snapshot, left(query,42) AS last_query FROM pg_stat_activity ...' pid | state | holds_own_xid | holds_snapshot | last_query
-----+---------------------+---------------+----------------+---------------------------------------
924 | idle in transaction | 773 | | CREATE TEMP TABLE t(i int);
970 | idle in transaction | | | SELECT count(*) FROM acct;
980 | idle in transaction | | 772 | SELECT count(*) FROM acct;
990 | idle in transaction | 776 | | UPDATE acct SET bal=bal+1 WHERE id=1;
(4 rows)| pid | Isolation | Wrote? | backend_xid | backend_xmin | Bounds the horizon? |
|---|---|---|---|---|---|
| 970 | READ COMMITTED | no | — | — | No |
| 980 | REPEATABLE READ | no | — | 772 | Yes, via its snapshot |
| 990 | READ COMMITTED | yes | 776 | — | Yes, via its own xid |
| 924 | READ COMMITTED | yes | 773 | — | Yes, via its own xid |
And the horizon itself confirms it:
$ psql -U postgres -d bank -c 'SELECT pg_snapshot_xmin(pg_current_snapshot()) AS oldest_running_xid, txid_current() AS current' oldest_running_xid | current
--------------------+---------
773 | 777
(1 row)The rule that falls out of this is short:
A session holds back cleanup when it holds a transaction ID or a snapshot.
idle in transactionis neither of those things; it is a state that frequently coincides with them.
READ COMMITTED takes a new snapshot per statement and releases it when the statement ends, so a read-only session sitting between statements holds nothing. REPEATABLE READ and SERIALIZABLE take one snapshot for the whole transaction and hold it until commit. Any session that has written has been assigned a transaction ID, and that ID is running until the transaction ends.
What every idle-in-transaction session holds regardless
Three costs apply whether or not the session bounds the horizon.
Locks. Every lock the transaction acquired is held until it ends.
A transaction that ran ALTER TABLE and then went idle holds an
exclusive lock, and every subsequent query on that table queues behind
it. This is the mechanism in Part IX, and it does not care about
isolation level or whether the session wrote afterwards.
A connection slot, counted against max_connections.
Its private memory, including whatever it allocated.
So an idle-in-transaction session is always worth investigating. The refinement is about which symptom to attribute to it.
idle_in_transaction_session_timeout
PostgreSQL will end these sessions for you.
$ psql -U postgres -c "SELECT name, setting, unit, context, short_desc FROM pg_settings WHERE name = 'idle_in_transaction_session_timeout'" name | setting | unit | context | short_desc
-------------------------------------+---------+------+---------+-------------------------------------------------------------
idle_in_transaction_session_timeout | 0 | ms | user | Sets the maximum allowed idle time between queries, when in a transaction.
(1 row)Because it is user context, the right place for it is usually a role
rather than the server:
-- Bound the workloads whose behaviour you know, individually
ALTER ROLE app_web SET idle_in_transaction_session_timeout = '30s';
ALTER ROLE reporting_ro SET idle_in_transaction_session_timeout = '5min';
ALTER ROLE migrator SET idle_in_transaction_session_timeout = '15min';
The related idle_session_timeout ends sessions idle outside a
transaction. It is a much blunter instrument and is usually wrong
against an application connection pool, because ending a pooled
connection that is idle by design forces the pool to reconnect for no
benefit. It is appropriate for interactive and ad-hoc roles.
Production discipline
- Sort by what is held, not by how long the session has been idle.
backend_xidandbackend_xminare the columns that decide whether a session is a vacuum problem. - Read
Client:ClientReadas “the database is waiting for the application”. Thequerycolumn shows the last statement, not a running one. - Treat every idle-in-transaction session as a locking and slot problem regardless, because those costs apply whatever the isolation level.
- Set
idle_in_transaction_session_timeoutper role, from measurement. A blanket value terminates migrations and exports that were behaving correctly. - Record
application_nameandclient_addrbefore terminating anything. Terminating resolves tonight and guarantees a repeat. - Treat a move to REPEATABLE READ as a vacuum decision too. It makes read-only long transactions bound the horizon, which READ COMMITTED does not.
Cross-course references
- Linux for Production Sysadmins — Part XXII (Network
troubleshooting) covers diagnosing a client that has stopped reading,
which is what
ClientReadon a long-idle session can indicate. - Observability for Production Sysadmins — Part XX (Alert quality) covers alerting on the oldest transaction age rather than on the count of idle sessions, which is the actionable signal.
- Kubernetes for Production Sysadmins — Part X (Termination) covers pods being killed mid-transaction, a common source of sessions that go idle and never return.
Quiz
Knowledge check · 6 questions
Q1. A session has been idle in transaction for two hours. pg_stat_activity shows backend_xid and backend_xmin both null. What does this establish?
Q2. A reporting workload is changed from READ COMMITTED to REPEATABLE READ so that multi-query reports see a consistent view. What side effect should be planned for?
Q3. Which costs does an idle-in-transaction session impose regardless of isolation level and regardless of whether it has written? Select all that apply.
Q4. The query column of a session in state idle in transaction shows the statement currently executing.
Q5. Why is a server-wide idle_in_transaction_session_timeout risky, and what is the better placement?
Q6. Separate the two problems in this evidence and give the order of action.
A table has grown from 40GB to 190GB over six weeks while its row count is unchanged, and autovacuum runs on it repeatedly without reclaiming space. Separately, a nightly deployment has begun timing out while waiting to alter that same table. pg_stat_activity shows eleven sessions idle in transaction. Of these, ten belong to the role app_web with backend_xid and backend_xmin both null and idle durations under four seconds. One belongs to the role analytics_ro, has been idle in transaction for nineteen hours, and reports backend_xmin populated.
Passing score: 75%. Answers are checked in this browser.