Postgres Error: 'there is already a transaction in progress' — Cause, Fix, and Troubleshooting Guide
Understand Postgres WARNING there is already a transaction in progress — a nested BEGIN inside an open transaction, ORM conflicts, and fixes.
- #postgres
- #postgresql
- #database
- #troubleshooting
Stuck on this Postgres error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
This is a WARNING, not an error. PostgreSQL emits it when a BEGIN arrives on a connection that is already inside an open transaction. The second BEGIN is ignored — the existing transaction continues unchanged — and the statement still “succeeds”.
WARNING: there is already a transaction in progress
Because it is only a warning, nothing fails and no rollback happens. That is exactly why it is dangerous: it almost always means your code has a mistaken mental model of who owns the transaction. The real bug is that a COMMIT or ROLLBACK later will end a transaction whose boundaries you did not intend.
Symptoms
- Logs fill with the
WARNINGabove but no query errors. - Changes commit or roll back at the “wrong” time — more or less work is atomic than expected.
- The reverse warning also appears on a stray
COMMIT/ROLLBACK:WARNING: there is no transaction in progress. - The pattern shows up right after adding manual
BEGINto code that runs under an ORM or a transaction-managing pool.
appdb=# BEGIN;
BEGIN
appdb=# BEGIN;
WARNING: there is already a transaction in progress
BEGIN
Common Root Causes
1. Nested BEGIN in application code
A helper opens a transaction with BEGIN, then calls another helper that also issues BEGIN. PostgreSQL does not support nesting transactions with BEGIN; the inner one is a no-op and warns.
BEGIN;
INSERT INTO orders (customer_id) VALUES (1);
BEGIN; -- WARNING: there is already a transaction in progress
INSERT INTO orders (customer_id) VALUES (2);
COMMIT; -- commits BOTH inserts; the second BEGIN did nothing
2. The driver/ORM already opened a transaction
Many drivers run with autocommit off, so the first statement implicitly starts a transaction. If your code then issues its own BEGIN, it lands inside the driver’s transaction and warns. This is the single most common source in ORM-managed apps.
3. Leaked / unclosed transactions
A previous code path opened a transaction and never committed or rolled back. The connection is returned to the pool still “in transaction”, and the next borrower’s BEGIN warns.
SELECT pid, state, now() - state_change AS in_state
FROM pg_stat_activity
WHERE state = 'idle in transaction';
4. Mixing manual BEGIN with a transaction-managing pool
Poolers and framework middleware (e.g., a per-request transaction) may wrap every request in BEGIN ... COMMIT. Adding a hand-written BEGIN inside that scope collides with the wrapper.
How to diagnose
Step 1: Check the connection’s transaction state
idle in transaction means the session already holds an open transaction — any BEGIN sent next will warn:
SELECT pid, usename, application_name, state,
now() - state_change AS in_state
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'idle in transaction (aborted)')
ORDER BY state_change;
pid | usename | application_name | state | in_state
-------+----------+------------------+---------------------+----------
40213 | app_user | orders-worker | idle in transaction | 00:04:11
(1 row)
A backend stuck in transaction for minutes is a leaked transaction that will trigger the warning on reuse.
Step 2: Reproduce and read the transaction status in psql
Turn on status display so you can see when a transaction is open:
appdb=# \set PROMPT1 '%/%R%x%# '
appdb=# BEGIN;
BEGIN
appdb*# BEGIN;
WARNING: there is already a transaction in progress
BEGIN
The * in the prompt means “in a transaction block”. A second BEGIN while * is showing is exactly the offending pattern.
Step 3: Watch for the reverse warning
If you also see stray commits warning, the same ownership confusion is ending transactions you did not start:
appdb=# COMMIT;
WARNING: there is no transaction in progress
COMMIT
Both warnings together are a strong signal that two layers are both trying to manage transaction boundaries.
Fixes
Let one layer own the transaction
Decide whether the driver/ORM or your code manages transactions — not both. If the framework wraps requests in a transaction, delete the manual BEGIN/COMMIT from application code and use the framework’s transaction block instead.
Use SAVEPOINT for real nesting
When you genuinely need nested, partially-rollbackable units of work, use savepoints instead of a second BEGIN:
BEGIN;
INSERT INTO orders (customer_id) VALUES (1);
SAVEPOINT add_second;
INSERT INTO orders (customer_id) VALUES (2);
-- roll back only the inner unit if needed:
ROLLBACK TO SAVEPOINT add_second;
COMMIT;
Savepoints are the supported way to get “nested transaction” semantics in PostgreSQL.
Do not issue BEGIN under a managing pool
If you connect through PgBouncer in transaction mode or an ORM session, do not hand-write BEGIN. Wrap your work in the ORM’s transaction API so a single owner emits exactly one BEGIN and one COMMIT.
Reap leaked transactions automatically
Prevent connections from sitting idle in transaction and poisoning the next borrower:
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();
Postgres will terminate a session left idle in a transaction past the timeout, surfacing the leak instead of silently carrying it into the pool.
What to watch out for
- This is a
WARNING— it will not fail your statement, so it is easy to ignore until commits land at the wrong boundary. Treat it as a bug. - The inner
BEGINis silently discarded; the outer transaction’sCOMMITstill commits everything since the firstBEGIN. - Its mirror image,
there is no transaction in progress, points at the same ownership confusion from the commit side. idle_in_transaction_session_timeoutrolls back and disconnects the offending session — make sure the app retries cleanly.- Under PgBouncer
transactionmode, session-level assumptions about an open transaction across statements will break; keep each transaction self-contained.
Related
- Postgres Error: ‘cannot execute … in a read-only transaction’
- Postgres Error: ‘deadlock detected’
- Postgres Error: ‘prepared statement already exists’
Fixed it? Get 500 Postgres & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.