You open pg_stat_activity and see sessions with state idle in transaction, some of them for hours. They aren't running anything, so they look harmless. They aren't. A single one can quietly bloat every busy table in the database and block your next migration.
What the state means
idle in transaction means the client ran BEGIN (explicitly, or implicitly through an ORM or driver with autocommit off), ran at least one statement, and then stopped talking without committing or rolling back. The transaction stays open until the client finishes it or the connection closes.
Common causes:
- Application code that opens a transaction, then calls an external API, sends an email or waits on a queue before committing.
- An exception path that returns the connection to the pool without rolling back.
- A developer's SQL client with autocommit disabled, left open overnight.
- A Python driver such as psycopg2, which starts a transaction on the first query by default.
Why it hurts: the xmin horizon
PostgreSQL's MVCC keeps old row versions around as long as any open transaction might still need to see them. The oldest running transaction's snapshot sets the xmin horizon. VACUUM can't remove dead rows newer than that horizon.
So one idle transaction from 9 a.m. means that every row updated or deleted since 9 a.m., in every table in that database, stays on disk as a dead tuple. On a busy queue or session table that's millions of rows. Indexes grow, sequential scans slow down, and autovacuum runs over and over without removing anything.
It also holds whatever locks it took. Even a plain SELECT takes an ACCESS SHARE lock, so a pending ALTER TABLE queues behind it, and every new query on that table queues behind the ALTER. That's how an idle session turns a two-second migration into an outage.
Find them
SELECT pid, usename, application_name, client_addr,
now() - xact_start AS xact_age,
now() - state_change idle_for,
(query, ) last_query
pg_stat_activity
state
xact_start;
