DEV Community

umzzil nng
umzzil nng

Posted on Originally published at oraerror.com

PostgreSQL 25004 Error: Causes and Solutions Complete Guide

PostgreSQL Error 25004: Inappropriate Isolation Level for Branch Transaction

PostgreSQL error code 25004 is raised when a transaction branch — typically part of a distributed XA transaction or a Two-Phase Commit (2PC) workflow — attempts to use an isolation level that is not permitted for that context. This error is a subclass of 25000 (invalid_transaction_state) and is strictly enforced to protect data consistency across distributed nodes. In practice, it most commonly surfaces in Java EE / Jakarta EE environments using JTA, or any middleware that manages distributed transactions on top of PostgreSQL.


Top 3 Causes

1. Changing Isolation Level After XA Branch Has Started

The most frequent cause is attempting to call SET TRANSACTION ISOLATION LEVEL after XA START has already been issued. PostgreSQL does not allow the isolation level to be modified once a branch transaction is active.

-- WRONG: Triggers error 25004
XA START 'branch_001';
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;  -- ERROR here!

-- CORRECT: Set isolation level before starting the branch
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
XA START 'branch_001';
INSERT INTO orders (order_id, amount) VALUES (2001, 4500);
XA END 'branch_001';
XA PREPARE 'branch_001';
XA COMMIT 'branch_001';
Enter fullscreen mode Exit fullscreen mode

2. Incorrect Isolation Level Timing in Two-Phase Commit (2PC)

When using PREPARE TRANSACTION, the isolation level must be declared at the very beginning of the transaction — inside the BEGIN statement. Attempting to change it mid-transaction before PREPARE will trigger this error.

-- WRONG: Setting isolation level after BEGIN
BEGIN;
INSERT INTO ledger (account_id, amount) VALUES (10, 2500);
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;  -- Too late, causes issues
PREPARE TRANSACTION 'dist_txn_42';

-- CORRECT: Declare isolation level with BEGIN
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO ledger (account_id, amount) VALUES (10, 2500);
UPDATE ledger SET amount = amount - 100 WHERE account_id = 5;
PREPARE TRANSACTION 'dist_txn_42';

-- On success from all nodes:
COMMIT PREPARED 'dist_txn_42';

-- Monitor prepared transactions
SELECT gid, prepared, owner FROM pg_prepared_xacts ORDER BY prepared;
Enter fullscreen mode Exit fullscreen mode

3. Middleware Forcing an Incompatible Isolation Level on the Branch

Connection poolers like Pgpool-II or certain ORM frameworks sometimes inject SET TRANSACTION ISOLATION LEVEL commands automatically at the session or branch level. If the middleware is configured to use READ UNCOMMITTED (which PostgreSQL silently maps to READ COMMITTED internally) or if the pooler sends isolation-level commands after a branch is already active, error 25004 can appear unexpectedly after upgrades or configuration changes.

-- Check current server default isolation level
SHOW default_transaction_isolation;

-- Align the session default to avoid conflicts from pooler
SET SESSION default_transaction_isolation = 'read committed';

-- Per-role or per-database alignment (run as superuser)
ALTER ROLE app_user SET default_transaction_isolation = 'read committed';
ALTER DATABASE myapp SET default_transaction_isolation = 'read committed';

-- Reload config if changed via ALTER SYSTEM
ALTER SYSTEM SET default_transaction_isolation = 'read committed';
SELECT pg_reload_conf();
Enter fullscreen mode Exit fullscreen mode

Quick Fix Solutions

  1. Always declare isolation level at BEGIN, not mid-transaction:
   BEGIN ISOLATION LEVEL READ COMMITTED;
   -- your DML here
   COMMIT;
Enter fullscreen mode Exit fullscreen mode
  1. For XA transactions, use SET SESSION CHARACTERISTICS before XA START to pre-configure the isolation level at the session scope.

  2. Audit your middleware configuration to ensure it does not inject isolation-level commands after a branch transaction has been opened.

  3. Clean up stale prepared transactions that may be blocking or causing cascading state issues:

   -- Find long-running prepared transactions
   SELECT gid, now() - prepared AS age, owner
   FROM pg_prepared_xacts
   WHERE now() - prepared > INTERVAL '30 minutes';

   -- Roll back a zombie prepared transaction
   ROLLBACK PREPARED 'stale_txn_gid';
Enter fullscreen mode Exit fullscreen mode

Prevention Tips

  • Enforce a coding standard: All transactions must declare their isolation level in the BEGIN statement. Add linting or code review rules to flag any SET TRANSACTION ISOLATION LEVEL calls inside an active transaction block.
  • Monitor pg_prepared_xacts continuously: Set up alerting for prepared transactions older than a defined threshold (e.g., 1 hour). Zombie prepared transactions are a leading indicator of misconfigured distributed transaction handling and can be an indirect cause of 25004 errors under load.

Related Errors

Code Name Relation
25000 invalid_transaction_state Parent class of 25004
25001 active_sql_transaction Triggered by disallowed ops in active txn
25P02 in_failed_sql_transaction Commands issued in a failed txn branch
40001 serialization_failure Common when using SERIALIZABLE in distributed txns

📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.

Top comments (0)