How to fix idle in transaction connections in PostgreSQL
Quick answer: An
idle in transactionconnection ranBEGINbut never committed or rolled back. While it sits there it holds any locks it took and pins thexminhorizon so autovacuum can't clean up dead tuples — quietly causing lock waits and bloat. Find them inpg_stat_activityand cap them withidle_in_transaction_session_timeout, then fix the app to commit promptly.
What the state actually means
A backend can be in several states. The dangerous one is idle in transaction:
active— running a query right now.idle— connected, no open transaction. Harmless.idle in transaction— ranBEGIN, maybe some statements, and is now waiting with the transaction still open. Harmful.idle in transaction (aborted)— same, but a statement errored; it's waiting forROLLBACK.
The usual cause is application code that opens a transaction and then does something slow inside it — an external API call, waiting on user input, a long compute — instead of committing quickly.
Why it hurts
An open transaction is not free:
- It holds locks. Any row or table locks it acquired stay held until commit, so other queries can block behind it.
- It blocks vacuum cleanup. The transaction keeps an old snapshot, which pins
the
xminhorizon: autovacuum can't remove dead tuples newer than that snapshot, so bloat builds up across the whole database while it sits there. - It occupies a connection slot you may badly need under load.
How do I find them?
SELECT pid,
state,
now() - xact_start AS txn_open_for,
now() - state_change AS idle_for,
query AS last_query
FROM pg_stat_activity
WHERE state LIKE 'idle in transaction%'
ORDER BY xact_start;
Match with LIKE 'idle in transaction%', not =. There's a second state —
idle in transaction (aborted), where a statement errored and the transaction is
sitting open waiting for a ROLLBACK — and an equality check silently misses it,
even though it does the same damage.
The oldest xact_start is doing the most damage — it's held its snapshot and
locks the longest. To end a specific offender:
SELECT pg_terminate_backend(<pid>);
Why does AI-generated code leave transactions open?
Your AI coding agent (Claude Code, Cursor) writes code that opens a transaction
and does the requested work — and sometimes that work includes a call to another
service, or an early return on an error path that skips the commit/rollback. It
reads as correct, transactional code. What it can't see is that in production that
external call takes two seconds while the transaction holds locks and blocks
vacuum. The cost is a runtime, concurrency property — invisible in the source.
How do I keep idle-in-transaction from coming back?
-
Set a timeout so a stuck transaction dies on its own —
idle_in_transaction_session_timeout(since PostgreSQL 9.6) terminates any session idle inside a transaction past the limit:ALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s'; -
Commit (or roll back) as soon as the database work is done.
-
Never hold a transaction open across a network call or user think-time.
How DBGorilla helps
DBGorilla connects read-only and gives your AI coding agent (Claude Code, Cursor)
the live session picture — which connections are idle in transaction, how long
they've held their snapshot, what locks they hold — so it can trace a stuck
transaction back to the code path that opened it and never committed. It surfaces
and explains; it does not terminate sessions or change server settings for you.
Get started free →
FAQ
What does idle in transaction mean?
A connection opened a transaction with BEGIN, ran some statements, and is now
idle without COMMIT or ROLLBACK — the transaction is still open.
Why is it a problem?
It holds locks (blocking others) and pins the xmin horizon so autovacuum can't
clean up dead tuples (causing bloat), and it ties up a connection.
How do I find them?
pg_stat_activity where state LIKE 'idle in transaction%' (use LIKE, not
=, so you also catch idle in transaction (aborted)), ordered by
now() - xact_start.
How do I kill them automatically?
Set idle_in_transaction_session_timeout (since PostgreSQL 9.6) at the server or
role level; sessions idle in a transaction past the limit are terminated.