Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Postgres By James Joyner IV · · 8 min read Last reviewed Jul 2026

Postgres Error: 'there is already a transaction in progress' — Cause, Fix, and Troubleshooting Guide

Quick answer

Understand Postgres WARNING there is already a transaction in progress — a nested BEGIN inside an open transaction, ORM conflicts, and fixes.

Part of the PostgreSQL Database Errors hub
  • #postgres
  • #postgresql
  • #database
  • #troubleshooting
Free toolkit

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 WARNING above 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 BEGIN to 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 BEGIN is silently discarded; the outer transaction’s COMMIT still commits everything since the first BEGIN.
  • Its mirror image, there is no transaction in progress, points at the same ownership confusion from the commit side.
  • idle_in_transaction_session_timeout rolls back and disconnects the offending session — make sure the app retries cleanly.
  • Under PgBouncer transaction mode, session-level assumptions about an open transaction across statements will break; keep each transaction self-contained.
Free download · 368-page PDF

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?

Free download · 368-page PDF

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.