When a distributed transaction branch is started with an isolation level that conflicts with the coordinating transaction or the constraints of the two-phase commit protocol, PostgreSQL raises ERROR: inappropriate isolation level for branch transaction with SQLSTATE 25004 and condition name inappropriate_isolation_level_for_branch_transaction. This error belongs to SQLSTATE class 25 — Invalid Transaction State.
What This Error Means
SQLSTATE class 25 covers errors related to invalid transaction state. Code 25004 specifically pertains to distributed (branched) transactions, which are associated with the SQL standard's concept of transaction branches used in two-phase commit (2PC) scenarios. In PostgreSQL, this surfaces most commonly through the PREPARE TRANSACTION / COMMIT PREPARED mechanism or through foreign data wrapper (FDW) connections that participate in a distributed transaction.
A branch transaction is a unit of work associated with a global transaction identifier (XID) that spans multiple resource managers or database connections. The SQL standard requires that all branches of a global transaction use compatible isolation levels. PostgreSQL enforces this constraint: if a branch is started with an isolation level that cannot be reconciled with the global transaction's requirements — for example, attempting to start a READ UNCOMMITTED or READ COMMITTED branch when the coordinating transaction demands SERIALIZABLE — the engine raises SQLSTATE 25004.
In practice, PostgreSQL does not implement READ UNCOMMITTED as a truly separate level (it is silently promoted to READ COMMITTED), and the actual enforcement of branch isolation compatibility depends heavily on the transaction manager coordinating the distributed work. When PostgreSQL is acting as a resource manager inside an XA transaction (via a JDBC XA datasource or a JTA transaction manager such as Atomikos or Narayana), the middleware layer transmits isolation level requests to each branch. If a branch receives an isolation level that PostgreSQL considers inappropriate for the current transaction state, 25004 is raised.
Common Causes
XA/JTA transaction manager misconfiguration. A Java EE or Jakarta EE application server or a standalone JTA provider (Atomikos, Bitronix, Narayana) is configured with a global transaction isolation level that conflicts with what was already set on the PostgreSQL connection or branch. For example, the XAConnection starts a branch with
TRANSACTION_SERIALIZABLEafter the global coordinator has already establishedTRANSACTION_READ_COMMITTEDon another branch.Attempting to change isolation level mid-branch. A driver or ORM issues
SET TRANSACTION ISOLATION LEVELafter a two-phase transaction branch has already been started withXAResource.start(), violating the rule that isolation level must be set before the first query of a transaction.Foreign Data Wrapper (FDW) branch mismatch. When
postgres_fdw(or another FDW) participates in a distributed transaction viaPREPARE TRANSACTION, the remote connection is opened as a transaction branch. If the local transaction runs atSERIALIZABLEbut the FDW connection is established atREAD COMMITTED, a branch isolation mismatch can result in this error on the remote side.Manual
PREPARE TRANSACTIONwith conflicting isolation levels. A developer or script explicitly usesBEGIN ISOLATION LEVEL SERIALIZABLE; ... PREPARE TRANSACTION 'txn-id';in one session and then tries to associate another branch at a lower isolation level with the same global transaction.
How to Fix inappropriate_isolation_level_for_branch_transaction
Align isolation levels across all branches. Ensure every branch of a global transaction uses the same isolation level. In a JTA application, set the isolation level once on the
UserTransactionorDataSourceand do not override it per-connection:// In a JTA/XA datasource configuration (e.g., Atomikos) // Set defaultIsolationLevel consistently for all XA connections xaDataSource.setDefaultIsolationLevel(Connection.TRANSACTION_READ_COMMITTED);Do not issue
SET TRANSACTIONafter a branch has started. The isolation level must be declared before any data-access statement in the transaction. In SQL, issue theSET TRANSACTIONorBEGIN ... ISOLATION LEVELstatement as the very first command:-- Correct: isolation level declared at BEGIN BEGIN ISOLATION LEVEL REPEATABLE READ; -- ... your statements ... PREPARE TRANSACTION 'my-global-txn';-- Incorrect: changing isolation level after work has begun BEGIN; SELECT * FROM orders WHERE id = 1; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- raises error in this contextConfigure
postgres_fdwto match the local isolation level. When usingpostgres_fdwwithPREPARE TRANSACTION, verify thatfdw_startup_costand connection options do not override isolation. Use thetransaction_isolationsession default or set it explicitly before the FDW queries execute:-- Set at session level before beginning the distributed transaction SET default_transaction_isolation = 'repeatable read';Review your transaction manager's XA branch start logic. In Atomikos or Narayana, check whether the connection pool applies a default isolation level that differs from what the application sets at the resource manager level. Ensure
xa.start()is called with a clean, consistently configured connection.
Additional Information
SQLSTATE class 25 (Invalid Transaction State) includes several related codes:
25000(generic invalid transaction state),25001(active_sql_transaction— e.g.,SET SESSIONinside a transaction),25002(branch_transaction_already_active),25003(inappropriate_access_mode_for_branch_transaction),25006(read_only_sql_transaction),25007(schema_and_data_statement_mixing_not_supported), and25P01/25P02(PostgreSQL-specificno_active_sql_transactionandin_failed_sql_transaction).SQLSTATE 25004 is defined by the SQL standard (SQL:1999 and later) and is not specific to PostgreSQL. However, because PostgreSQL's native transaction model is single-database and primarily single-phase, this error is rarely triggered in simple setups. It almost exclusively appears in XA/JTA-managed environments or explicit two-phase commit workflows.
PostgreSQL has supported
PREPARE TRANSACTION(two-phase commit) since version 8.0. Themax_prepared_transactionsGUC must be set to a non-zero value to use it; the default is0in many distributions, which disables 2PC entirely. If 2PC is disabled, you cannot create the branch conditions that lead to 25004.Most ORM frameworks (Hibernate, SQLAlchemy, ActiveRecord) do not use XA transactions directly and will not produce this error under normal usage. It is almost always triggered by middleware transaction managers or explicit low-level JDBC/ODBC XA API usage.
Frequently Asked Questions
Why does this error only appear in my application server environment and not in psql?
SQLSTATE 25004 requires a distributed (multi-branch) transaction context, which is created by XA/JTA transaction managers or explicit PREPARE TRANSACTION calls. When you run queries directly in psql, you are always in a single-branch local transaction, so the branch isolation constraints never come into play.
Does PostgreSQL actually distinguish between READ UNCOMMITTED and READ COMMITTED?
No. PostgreSQL treats READ UNCOMMITTED as equivalent to READ COMMITTED internally — dirty reads are never permitted. However, when an XA transaction manager explicitly requests READ UNCOMMITTED as the branch isolation level via the JDBC XAResource API, PostgreSQL may still raise 25004 if the coordination protocol considers this level incompatible with the global transaction state.
Can I see which prepared transactions are currently open on my server?
Yes. Query the pg_prepared_xacts system view:
SELECT gid, owner, database, prepared, transaction
FROM pg_prepared_xacts;
This helps diagnose orphaned prepared transactions that might be holding locks or contributing to branch state issues.
Is there a way to disable isolation level enforcement for XA branches? No — this is mandated by the SQL standard and enforced by PostgreSQL's transaction state machine. The correct fix is to ensure consistent isolation levels across all branches rather than attempting to bypass the check.