PostgreSQL Invalid Transaction Initiation (SQLSTATE 0B000)

PostgreSQL raises SQLSTATE 0B000 (invalid_transaction_initiation) when a BEGIN or START TRANSACTION command is issued in a context where starting a new transaction is not permitted. The server returns an error such as:

WARNING:  there is already a transaction in progress

or, in certain procedural contexts:

ERROR:  invalid transaction initiation

The condition name is invalid_transaction_initiation and it belongs to SQLSTATE class 0B — a standalone class defined in the SQL standard for transaction initiation errors.

What This Error Means

In PostgreSQL, every statement outside of an explicit transaction block runs in its own implicit single-statement transaction. When a client explicitly issues BEGIN or START TRANSACTION, PostgreSQL enters a multi-statement transaction block. Issuing another BEGIN while already inside a transaction block is not allowed by the SQL standard, and PostgreSQL enforces this.

In interactive psql sessions, PostgreSQL typically downgrades this to a WARNING ("there is already a transaction in progress") and continues rather than aborting. However, in procedural code — PL/pgSQL functions, procedures, or trigger functions — attempting to start a transaction in a context that does not support it raises a hard ERROR with SQLSTATE 0B000.

The most common procedural scenario is calling BEGIN inside a PL/pgSQL function body. Ordinary PL/pgSQL functions always execute within the calling transaction; they cannot start or commit their own transactions. Only procedures invoked with CALL (introduced in PostgreSQL 11) and DO blocks can manage their own transactions using COMMIT and ROLLBACK — and even then, only when called from a top-level context, not from within another transaction block.

Common Causes

  1. Calling BEGIN inside a PL/pgSQL function. Functions run as part of their caller's transaction and do not have the authority to start a new one. Any BEGIN statement in function body code triggers this error.

  2. Using BEGIN inside a procedure called from within a transaction block. Even though procedures can manage transactions, they cannot do so if the CALL statement itself was made inside an open transaction. The caller's transaction takes precedence.

  3. Application-level double BEGIN. A connection pooler or ORM issues BEGIN before calling a stored procedure or function that also issues BEGIN internally. This commonly surfaces in frameworks that automatically wrap calls in transactions.

  4. Trigger functions attempting to control transactions. Trigger functions execute inside the transaction that fired the trigger and cannot initiate new transactions.

  5. DO block issued inside an existing transaction. A DO block that calls COMMIT or uses transaction control will fail if it was started inside an open transaction.

How to Fix invalid_transaction_initiation

  1. Remove BEGIN from PL/pgSQL function bodies. Functions cannot control transactions — remove any BEGIN/COMMIT/ROLLBACK statements and let the caller manage the transaction:

    -- Wrong: BEGIN inside a function
    CREATE OR REPLACE FUNCTION do_work() RETURNS void AS $$
    BEGIN
        BEGIN;  -- ERROR: invalid transaction initiation
        INSERT INTO logs(msg) VALUES ('started');
        COMMIT;
    END;
    $$ LANGUAGE plpgsql;
    
    -- Correct: remove transaction control from the function
    CREATE OR REPLACE FUNCTION do_work() RETURNS void AS $$
    BEGIN
        INSERT INTO logs(msg) VALUES ('started');
    END;
    $$ LANGUAGE plpgsql;
    
  2. Convert the function to a procedure if transaction control is needed. Procedures (PostgreSQL 11+) called with CALL from a top-level context can use COMMIT and ROLLBACK:

    CREATE OR REPLACE PROCEDURE do_work_with_txn() LANGUAGE plpgsql AS $$
    BEGIN
        INSERT INTO logs(msg) VALUES ('step 1');
        COMMIT;
        INSERT INTO logs(msg) VALUES ('step 2');
        COMMIT;
    END;
    $$;
    
    -- Call from a top-level context (not inside BEGIN...COMMIT)
    CALL do_work_with_txn();
    
  3. Ensure the procedure is called outside any transaction block. If an application framework wraps every call in BEGIN/COMMIT, disable that behavior for the specific CALL statement, or refactor the logic so that transaction management happens at only one layer.

  4. Check ORM and connection pooler settings. Frameworks like SQLAlchemy, Django ORM, ActiveRecord, and connection poolers like PgBouncer or pgpool-II may issue automatic BEGIN statements. Inspect the actual SQL being sent (enable log_statements = 'all' temporarily) to confirm whether a BEGIN is already in flight before your code issues another one.

Additional Information

  • SQLSTATE class 0B contains only the single condition invalid_transaction_initiation — there are no sibling codes within this class.
  • Transaction control in procedures (COMMIT/ROLLBACK within CALL) was introduced in PostgreSQL 11. On earlier versions, no server-side code could manage transactions at all.
  • Related transaction-state errors include 25001 (active_sql_transaction) — raised when a command requires no active transaction but one exists — and 25P02 (in_failed_sql_transaction) — raised when a command is issued after a transaction has already entered an error state.
  • Most PostgreSQL client drivers (libpq, psycopg2, asyncpg, node-postgres) surface this as a database exception containing the SQLSTATE 0B000. The exact Python exception class is psycopg2.errors.InvalidTransactionInitiation.
  • In psql, the interactive client, issuing BEGIN twice results in a WARNING rather than an error, which can mask the problem during manual testing but still cause failures in application code.

Frequently Asked Questions

Why does BEGIN work fine in psql but fail in my application code?

In psql, a redundant BEGIN is downgraded to a warning and execution continues. Inside PL/pgSQL functions or procedures called within a transaction, PostgreSQL enforces the restriction as a hard error. The difference is context: psql is a client issuing top-level commands; a function body runs server-side within a transaction that the function does not own.

Can I use SAVEPOINT instead of nested BEGIN?

Yes. PostgreSQL does not support true nested transactions, but it does support savepoints within a transaction block. Use SAVEPOINT name, RELEASE SAVEPOINT name, and ROLLBACK TO SAVEPOINT name to create rollback points inside a transaction without starting a new one.

BEGIN;
  INSERT INTO orders(item) VALUES ('widget');
  SAVEPOINT before_payment;
  INSERT INTO payments(amount) VALUES (9.99);
  -- If payment fails:
  ROLLBACK TO SAVEPOINT before_payment;
COMMIT;

My procedure needs to commit partway through. Is that supported?

Yes, but only when the procedure is called from outside any transaction block. Call CALL my_procedure() directly from your application without wrapping it in BEGIN/COMMIT. Inside the procedure, use COMMIT and ROLLBACK freely. If the CALL is inside an open transaction, PostgreSQL will raise an error when the procedure tries to commit.

How do I find out which layer is issuing the extra BEGIN?

Enable verbose query logging temporarily:

ALTER SYSTEM SET log_min_messages = 'debug1';
ALTER SYSTEM SET log_statements = 'all';
SELECT pg_reload_conf();

Then check postgresql.log. You will see every statement the server receives, making it straightforward to identify which client or framework is issuing BEGIN before your code does. Remember to reset these settings afterward, as they generate significant log volume.

Subscribe to the Pulse Newsletter

Get early access to new Pulse features, insightful blogs & exclusive events , webinars, and workshops.

We use cookies to provide an optimized user experience and understand our traffic. To learn more, read our use of cookies; otherwise, please choose 'Accept Cookies' to continue using our website.