PostgreSQL
PostgreSQL — ERROR: current transaction is aborted, commands ignored until end of transaction block
Written and reviewed by Sahil Srivastav
ERROR: current transaction is aborted, commands ignored until end of transaction block
STATEMENT: SELECT * FROM orders WHERE id = $1What this error actually means
This error tells you nothing about what went wrong. A statement inside your transaction failed earlier, PostgreSQL marked the transaction as aborted, and every subsequent statement is refused until you issue `ROLLBACK` or `COMMIT` (which, in an aborted transaction, also rolls back). What you are seeing is the aftermath.
PostgreSQL behaves this way deliberately. Once any statement in a transaction fails, continuing would mean the transaction’s remaining work is built on a state you did not verify. Rather than letting you accumulate work on a broken foundation, the server refuses everything and forces you to decide explicitly: abandon the transaction, or rewind to a savepoint taken before the failure.
So the practical task is always the same: find the *first* error in the sequence. If your logs show this message repeated fifty times, the interesting line is the one immediately before the first occurrence — and if your error handling caught and swallowed it, you will have to go and stop doing that before you can diagnose anything.
Causes, most common first
- 1An earlier statement failed and the error was swallowed. The dominant cause. A `try`/`except` or `catch` around a single statement logs at debug level or ignores the exception, then execution continues in the same transaction. The original constraint violation or type error never reaches anyone.
- 2Retrying the failed statement inside the same transaction. A retry wrapper around the statement rather than the transaction. The transaction is already aborted, so every attempt returns this error — producing a loop that looks like an infinite retry bug and is really a scoping bug.
- 3Batch insert where one row violates a constraint. A loop inserting thousands of rows in one transaction. Row 4,312 violates a unique constraint, and rows 4,313 onward all report this instead. Without savepoints there is no way to skip the bad row and keep the good ones.
- 4A connection returned to the pool in an aborted state. The transaction was never rolled back before release, so the next request inherits a poisoned connection. Symptoms then appear in a completely unrelated endpoint, which is why this one takes so long to track down.
- 5A probe query run after a failure to “check” something. Error handling that tries to query state after catching a failure — checking whether a row exists, reading a sequence value, writing an audit record. That probe itself fails with this error, often masking the original exception in the process.
When you see it
- Logs full of this one message with no other error visible, because the real one was caught and discarded
- Every query on a pooled connection fails until the connection is recycled
- A retry loop that retries individual statements inside the same transaction and fails identically every time
- It appears in bulk or batch paths where one bad row poisons the rest of the batch
- Python drivers surface it as `InFailedSqlTransaction`; JDBC as a generic `PSQLException` on every subsequent call
How to diagnose it
Step 1
Find the first error, not the repeated one
Search the server log for the earliest error on the same session pid within the transaction window. That is the real failure; everything after it is noise.
grep -n 'ERROR' /var/log/postgresql/postgresql.log | grep -v 'current transaction is aborted' | tail -40Step 2
Stop swallowing statement errors
If the first error is not in your application logs, your handler discarded it. Re-raise or log at error level with the full driver exception — the constraint name and column are in there and usually identify the bug immediately.
Step 3
Check the transaction state the driver reports
Most drivers expose the transaction status. Logging it before each statement in a suspect path shows exactly where the transition to aborted happened.
SELECT pid, state, xact_start FROM pg_stat_activity WHERE pid = pg_backend_pid();Step 4
Look for connections released while aborted
If the failure appears in an endpoint that does not own the failing statement, the connection was returned dirty. Assert in tests that transaction status is idle at release.
The fix
Fix the first error. Everything else here is secondary — this message is a symptom of an earlier failure that was mishandled, so the real work is making that failure visible. Stop catching statement-level exceptions without re-raising or logging them at error level.
Retry at the transaction level, never the statement level. Roll back, then open a fresh transaction and replay the whole unit of work. A retry inside an aborted transaction cannot succeed by construction, and code that tries reveals a misunderstanding of the failure model rather than a flaky database.
For batches where individual rows may legitimately fail, use savepoints. Take a savepoint before each row, and on failure `ROLLBACK TO SAVEPOINT` to rewind just that row and continue. This is what makes partial-success batches possible — note that savepoints have real per-statement cost, so for large batches prefer validating input up front or using `INSERT ... ON CONFLICT DO NOTHING` to make the conflict a non-error.
Guarantee rollback before the connection is released. A `finally` block that rolls back on any failure path, or declarative transaction management that owns the whole boundary, prevents an aborted transaction from poisoning a pooled connection and surfacing in someone else’s endpoint.
Never run a probe query after catching a database error inside the same transaction. Roll back first, then investigate on a clean transaction.
-- One bad row aborts the whole batch
BEGIN;
INSERT INTO orders (...) VALUES (...); -- row 4312 violates a unique constraint
INSERT INTO orders (...) VALUES (...); -- ERROR: current transaction is aborted
COMMIT;
-- Savepoint per row: rewind the bad one, keep the rest
BEGIN;
SAVEPOINT row_sp;
INSERT INTO orders (...) VALUES (...);
-- on failure:
ROLLBACK TO SAVEPOINT row_sp; -- transaction is usable again
-- on success:
RELEASE SAVEPOINT row_sp;
COMMIT;How to stop it coming back
- Never catch a database exception without logging the driver error at error level — this message exists mostly because people do
- Scope retries to transactions, and make that the shape of your retry helper so statement-level retry is not expressible
- Assert in integration tests that every connection is released with transaction status idle
- Prefer `ON CONFLICT` and up-front validation over savepoint-per-row for large batches
- Let the framework own transaction boundaries; hand-rolled `begin`/`commit` is where these bugs live
FAQ
Why does PostgreSQL not just skip the failed statement?
Because the rest of the transaction would then be built on state you never verified, and the transaction would commit a mixture of applied and skipped work — with no record of which. Refusing everything forces an explicit decision. Savepoints exist precisely so you can opt into partial recovery where it makes sense.
Why does my retry loop fail forever?
Because it retries the statement, not the transaction. The transaction is already aborted, so every attempt gets this same error regardless of whether the underlying problem was transient. Roll back and replay the whole unit of work.
Are savepoints expensive?
Enough to matter at volume. Each one has per-statement overhead and consumes subtransaction resources, and tens of thousands within one transaction degrade performance noticeably. For large loads, validate up front or use `ON CONFLICT DO NOTHING` so the failure never happens.
Why does this show up in an endpoint that does no writes?
A connection was returned to the pool with an aborted transaction still open. The next borrower inherits it and fails on its first statement. Guaranteeing rollback before release is what fixes it — the read-only endpoint is a victim, not the cause.
Related
Other errors engineers hit next to this one
- RejectedExecutionException: Task rejected from ThreadPoolExecutor
- Found one Java-level deadlock (thread dump)
- Threads blocked forever on a ReentrantLock with no deadlock reported
- ThreadLocal value leaking across requests on a pooled thread
- Consumer stuck in Object.wait() with work already in the queue
- java.lang.IllegalMonitorStateException: current thread is not owner
- Thread pool starvation — every worker waiting on a task in its own pool
- Partially constructed object published by double-checked locking