PostgreSQL raises ERROR: invalid transaction termination with SQLSTATE 2D000 and condition name invalid_transaction_termination when code attempts to commit or roll back a transaction from a context that does not permit direct transaction control — most commonly from inside a function that was called by a trigger, or from within a PL/pgSQL function invoked as part of a query.
What This Error Means
SQLSTATE 2D000 belongs to class 2D — a single-member error class dedicated solely to this condition. PostgreSQL's transaction model distinguishes between two kinds of procedural contexts: those that own a transaction (such as DO blocks executed at top level, or procedures called via CALL) and those that are subordinate to a caller's transaction (functions called from SQL queries, trigger functions, and most PL/pgSQL functions invoked as part of a query).
When code in a subordinate context issues a COMMIT or ROLLBACK statement, PostgreSQL rejects the attempt with 2D000 because the transaction lifecycle belongs to the caller, not to the function. This is a hard constraint of the transaction model: a function that participates in a query is executed inside the caller's transaction, and allowing it to commit or roll back would leave the calling statement in an undefined state.
The connection remains usable after this error. Because 2D000 is raised before any transaction boundary is crossed, the outer transaction is still active and can be committed or rolled back normally by the application.
Common Causes
Calling
COMMITorROLLBACKinside a trigger function. Trigger functions always execute within the transaction that fired the trigger. Any attempt to terminate that transaction from inside the trigger body raises2D000.Using
COMMIT/ROLLBACKinside a PL/pgSQL function called from a SQL query. Regular PL/pgSQL functions (created withCREATE FUNCTION) run within the caller's transaction. Only stored procedures (created withCREATE PROCEDUREand invoked viaCALL) support autonomous transaction control in PostgreSQL 11+.Calling a procedure that issues
COMMIT/ROLLBACKfrom inside a trigger or function. Even if theCOMMITis inside a procedure, calling that procedure from a trigger or from another function that doesn't own its transaction propagates the restriction.Using
PERFORMor direct calls to a transaction-managing procedure from within a non-procedure context, such as from inside aDOblock that was itself invoked from a function.
How to Fix invalid_transaction_termination
Move transaction control to a stored procedure. If you need to commit mid-operation, restructure the logic as a
CREATE PROCEDUREinvoked viaCALLrather than aCREATE FUNCTION. Procedures supportCOMMITandROLLBACKin PostgreSQL 11 and later:CREATE PROCEDURE process_batch() LANGUAGE plpgsql AS $$ BEGIN -- do some work INSERT INTO audit_log(event) VALUES ('step 1'); COMMIT; -- do more work INSERT INTO audit_log(event) VALUES ('step 2'); COMMIT; END; $$; CALL process_batch();Remove transaction control from trigger functions. Trigger functions must not contain
COMMIT,ROLLBACK, or calls to procedures that issue them. If you need side effects that survive even if the triggering transaction rolls back, consider usingpg_background(an extension), logical replication, or a deferred job queue processed outside the transaction.Use savepoints instead of full commits where partial rollback is needed. Inside a function or trigger,
SAVEPOINTandROLLBACK TO SAVEPOINTare permitted and can provide partial rollback semantics without terminating the outer transaction:BEGIN; SAVEPOINT sp1; -- attempt risky operation INSERT INTO t VALUES (1); -- if needed: ROLLBACK TO SAVEPOINT sp1; -- continue with outer transaction COMMIT;Audit any dynamic SQL (
EXECUTE) that might issueCOMMIT/ROLLBACK. Dynamic SQL executed viaEXECUTEinside a function inherits the same transaction ownership restrictions. Review any dynamically constructed statements.
Additional Information
- Transaction control in procedures (
COMMIT/ROLLBACKinsideCREATE PROCEDURE) was introduced in PostgreSQL 11. On PostgreSQL 10 and earlier, there is no supported way to commit mid-procedure, and all such attempts raise2D000regardless of context. - Related SQLSTATE codes in the transaction management area:
25000(INVALID_TRANSACTION_STATE),25001(ACTIVE_SQL_TRANSACTION),25006(READ_ONLY_SQL_TRANSACTION), and3B000(SAVEPOINT_EXCEPTION). - Most client drivers (libpq, psycopg2, asyncpg, JDBC, node-postgres) surface this error with the original message text and SQLSTATE. The exception class in Python's psycopg2 is
psycopg2.errors.InvalidTransactionTermination. - There are no performance implications to
2D000itself — it is raised immediately without modifying any data. However, the outer transaction remains open and holds any locks it has acquired until the application explicitly commits or rolls it back.
Frequently Asked Questions
Why can't I use COMMIT inside a trigger function?
Trigger functions execute as part of the statement that fired the trigger, which is itself inside a transaction. Committing from the trigger would commit all changes made by the outer statement — including potentially incomplete changes — and would leave PostgreSQL unable to provide the atomicity guarantees that triggers rely on. The restriction is fundamental to how PostgreSQL integrates triggers into the transaction model.
My code uses CREATE PROCEDURE but still gets 2D000 — why?
Procedures support autonomous transaction control only when invoked directly via CALL at the top level of a transaction. If the procedure is called from inside another function, a trigger, or another procedure that was itself called from a non-procedure context, the transaction ownership restriction is inherited. Check the full call chain to ensure that CALL originates from application code or a top-level DO block, not from inside a SELECT or another function.
Can I use dblink or pg_background to work around this inside a trigger?
Yes, with caveats. dblink opens a separate database connection and can commit independently of the current transaction. pg_background (a community extension) spawns a background worker. Both approaches provide an escape hatch for cases where you genuinely need durable side effects from a trigger, but they add complexity and should be used only when simpler designs (deferred jobs, post-commit hooks at the application layer) are not feasible.
Does this error leave my transaction in an aborted state?
No. Unlike many other errors that abort the current transaction and require a ROLLBACK before the connection can be reused, 2D000 is raised before any transaction boundary is modified. The outer transaction remains active and in its previous state. You can continue issuing statements or commit/roll back as normal after catching this error.