ArmorDB Logo
ArmorDB
Fix Postgresql Idle In Transaction
Fix PostgreSQL Idle in Transaction Sessions Before They Hurt Production
Back to Blog
Short-Form & Quick Fixes
June 26, 2026
5 min read

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.

AE
ArmorDB EngineeringArmorDB engineering
PostgreSQLTransactionsLocks

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.

SituationWhat it usually meansSafer response
Old session from an admin consoleSomeone opened BEGIN and walked awayConfirm owner, then terminate if abandoned
Web request connection left idleApplication code did work inside a transaction and stopped before commitRoll back through the app if possible; terminate if it is blocking production
Worker or migration connectionA job may be paused between statementsCheck deploy/job logs before terminating
Many sessions from the same serviceTransaction handling bug or pool misuseReduce traffic or roll back the deploy, then fix code
Sessions holding lock queuesOther work is waiting behind themPrioritize 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

Topic

Short-Form & Quick Fixes

Updated

Jun 26, 2026

Read time

5 min read

About the author

ArmorDB Engineering writes about PostgreSQL operations, security, and infrastructure decisions for teams building production apps on ArmorDB.