Fix PostgreSQL Idle in Transaction Sessions Before They Hurt Production
Learn how to find PostgreSQL idle in transaction sessions, recover safely, set guardrails, and fix the application patterns that leave transactions open.
An idle in transaction PostgreSQL session is a connection that began a transaction, stopped running SQL, and did not commit or roll back. It can look harmless because the session is not actively consuming CPU. In production, it can still hold locks, keep old row versions visible, delay vacuum cleanup, and make later schema changes or writes behave unpredictably.
The immediate fix is not to kill every idle session. First identify what the session is holding, whether it belongs to a user request, job, migration, or console, and whether rolling it back is safe. Then fix the code path that left the transaction open so the same state does not return after the next deploy.
Confirm the state and the owner
PostgreSQL exposes session state through pg_stat_activity. A session with state = 'idle in transaction' has an open transaction but no currently running statement. The most useful fields are xact_start, state_change, application_name, client_addr, backend_type, and the last query text. xact_start tells you how long the transaction has existed; state_change tells you how long it has been idle.
During an incident, sort by the oldest transaction first. A five-second idle transaction from a normal request may disappear on its own. A thirty-minute idle transaction from a worker, migration, or interactive console deserves attention because it may be pinning snapshots and holding locks acquired earlier in the transaction.
A focused inspection query usually starts like this:
SELECT pid, usename, application_name, client_addr, state, xact_start, state_change, wait_event_type, query FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY xact_start NULLS LAST;
Treat the query field as a clue, not a complete audit log. It often shows the last statement, not every statement that ran inside the transaction. If the last query was a harmless SELECT, the session may still hold locks from an earlier UPDATE or DDL statement in the same transaction.
Decide whether to wait, cancel, or terminate
The recovery choice depends on ownership and business context. PostgreSQL provides functions to cancel a running backend or terminate a backend, but an idle session has no running statement to cancel. Terminating the backend closes the session and rolls back its open transaction, which releases locks and snapshots. That is exactly what you want for an abandoned console. It may be wrong for a payment, billing, import, or migration transaction that is paused because the application is waiting on something else.
| Situation | What it usually means | Safer response |
|---|---|---|
| Old session from an admin console | Someone opened BEGIN and walked away | Confirm owner, then terminate if abandoned |
| Web request connection left idle | Application code did work inside a transaction and stopped before commit | Roll back through the app if possible; terminate if it is blocking production |
| Worker or migration connection | A job may be paused between statements | Check deploy/job logs before terminating |
| Many sessions from the same service | Transaction handling bug or pool misuse | Reduce traffic or roll back the deploy, then fix code |
| Sessions holding lock queues | Other work is waiting behind them | Prioritize releasing the blocker over changing global timeouts |
If you terminate, record the PID, application name, transaction age, and suspected owner. That post-incident note is often the fastest path to the real fix because it points directly at the service or workflow that left the transaction open.
Add guardrails with idle-in-transaction timeout
PostgreSQL includes idle_in_transaction_session_timeout, a client connection default that terminates sessions which stay idle inside a transaction longer than the configured duration. It is a useful guardrail because it turns an unbounded failure mode into a bounded one. It is not a substitute for correct transaction handling.
Set the value by role, database, connection string, or session depending on scope. For most applications, a role-level or database-level default is easier to reason about than relying on every code path to set it manually. Avoid choosing a value so short that legitimate multi-statement work fails under normal latency. Also avoid leaving it disabled for production application roles unless another layer reliably enforces transaction boundaries.
A practical pattern is to use a modest timeout for web roles, a different budget for background jobs, and explicit exceptions for maintenance sessions. Migrations and data repairs should be intentional about their transaction scope rather than inheriting the same behavior as request traffic.
Fix the application pattern
Most repeat idle in transaction incidents come from doing non-database work while a transaction is open. A request starts a transaction, writes a row, calls another API, renders a response, waits on a queue, or performs file I/O, and only then commits. If anything in that middle section hangs, PostgreSQL sees an open transaction doing nothing.
The durable fix is to keep transactions narrow. Open the transaction as late as possible, run the SQL that must be atomic, commit or roll back, and then do slow external work outside the transaction. If external work must happen before the commit, use clear timeouts and make the workflow idempotent so a rollback is safe. For ORMs, audit helpers that implicitly start transactions around larger blocks of application code than the developer expects.
Pooling adds one more edge. With application pools or PgBouncer, a leaked transaction can tie up a server connection and make unrelated requests queue behind it. ArmorDB includes PgBouncer for connection management, but PgBouncer cannot make an open PostgreSQL transaction safe. If idle transactions appear together with connection pressure, review the connection pool sizing guide and the PgBouncer documentation after the immediate blocker is gone.
Validate the fix
After recovering, watch pg_stat_activity during the same workload that caused the incident. The count of idle in transaction sessions should return to zero quickly, and the oldest transaction age should stay within the budget you expect. Also check whether vacuum is catching up if the old transaction was present for a long time. Long-lived transactions can prevent cleanup of dead tuples, so the performance impact may last after the session itself is gone.
The final validation is a code or runbook change, not only a clean dashboard. Add a regression test around transaction closure if the bug was in application code. If the cause was an admin workflow, document that consoles must commit or roll back before being left open. If the cause was a migration, change the migration template so lock and transaction behavior are explicit.
Takeaway
An idle in transaction session is PostgreSQL telling you that work began but the transaction boundary was never closed. Recover carefully by identifying the owner and terminating only when rollback is acceptable. Prevent repeats by narrowing transaction scope, setting idle_in_transaction_session_timeout for application roles, and treating long transaction age as a production signal alongside locks and connection usage.
Sources and further reading
- PostgreSQL documentation: client connection defaults and
idle_in_transaction_session_timeout: https://www.postgresql.org/docs/current/runtime-config-client.html - PostgreSQL documentation: monitoring statistics and
pg_stat_activity: https://www.postgresql.org/docs/current/monitoring-stats.html - PostgreSQL documentation: explicit locking behavior: https://www.postgresql.org/docs/current/explicit-locking.html
- PostgreSQL documentation: terminating backend processes: https://www.postgresql.org/docs/current/functions-admin.html
Topic
Short-Form & Quick Fixes
Updated
Jun 26, 2026
Read time
5 min read
ArmorDB Engineering writes about PostgreSQL operations, security, and infrastructure decisions for teams building production apps on ArmorDB.
Read next
Tech-News & Trends · 7 min read
PostgreSQL 18 pg_upgrade Statistics: Safer Major Upgrades
PostgreSQL 18 lets pg_upgrade retain most optimizer statistics. Learn what changes, what still needs ANALYZE, and how to update a production upgrade runbook.
Read articleData-Specs · 8 min read
PostgreSQL Isolation Levels Compared for SaaS Applications
Compare PostgreSQL Read Committed, Repeatable Read, and Serializable isolation for production SaaS workloads, including retries, locks, and when each level fits.
Read article