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 (INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION) occurs when a branch transaction in a distributed transaction environment attempts to use an isolation level other than SERIALIZABLE. This error is most commonly encountered in Two-Phase Commit (2PC) workflows and XA transaction manager integrations. If your middleware or application sets a lower isolation level before calling PREPARE TRANSACTION, PostgreSQL will immediately reject it with this error.


Top 3 Causes

1. Using PREPARE TRANSACTION Without SERIALIZABLE Isolation

PostgreSQL only permits SERIALIZABLE isolation for branch transactions participating in 2PC. Running PREPARE TRANSACTION while in READ COMMITTED or REPEATABLE READ mode will trigger error 25004.

-- ❌ Fails: default isolation level is READ COMMITTED
BEGIN;
INSERT INTO orders (customer_id, amount) VALUES (10, 250.00);
PREPARE TRANSACTION 'txn_001';  -- ERROR 25004!

-- ✅ Correct: explicitly set SERIALIZABLE
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO orders (customer_id, amount) VALUES (10, 250.00);
PREPARE TRANSACTION 'txn_001';  -- OK

-- Commit or rollback the prepared transaction
COMMIT PREPARED 'txn_001';
Enter fullscreen mode Exit fullscreen mode

2. XA DataSource Configured With Wrong Isolation Level

In Java EE / Jakarta EE environments using XA datasources (Atomikos, JBoss, WebLogic), the defaultTransactionIsolation property is often set to TRANSACTION_READ_COMMITTED. When the XA transaction manager attempts to prepare a branch, PostgreSQL rejects the non-serializable isolation level.

-- Check the current session isolation level in PostgreSQL
SHOW transaction_isolation;

-- Force SERIALIZABLE at the session level as a workaround
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Now the XA branch can be safely prepared
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 500 WHERE id = 7;
PREPARE TRANSACTION 'xa_branch_transfer_007';
COMMIT PREPARED 'xa_branch_transfer_007';
Enter fullscreen mode Exit fullscreen mode

3. Server or Session Default Isolation Level Changed

A DBA or developer may have altered the default isolation level at the server (postgresql.conf) or session level, unknowingly breaking all 2PC workflows that rely on the serializable default.

-- Check the server-wide default
SELECT name, setting, source
FROM pg_settings
WHERE name = 'default_transaction_isolation';

-- Check for stuck prepared transactions caused by the misconfiguration
SELECT gid, prepared, owner, database,
       EXTRACT(EPOCH FROM (NOW() - prepared)) AS pending_seconds
FROM pg_prepared_xacts
ORDER BY prepared;

-- Clean up orphaned prepared transactions
ROLLBACK PREPARED 'txn_orphan_stale';
Enter fullscreen mode Exit fullscreen mode

Fix in postgresql.conf:

default_transaction_isolation = 'serializable'
Enter fullscreen mode Exit fullscreen mode

Then reload without a full restart:

SELECT pg_reload_conf();
SHOW default_transaction_isolation;  -- should return: serializable
Enter fullscreen mode Exit fullscreen mode

Quick Fix Summary

Cause Fix
Wrong isolation in BEGIN Use BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE
XA datasource config Set defaultTransactionIsolation=SERIALIZABLE in datasource
postgresql.conf default Set default_transaction_isolation = 'serializable' and reload

Prevention Tips

Enforce isolation level in code: Always explicitly declare ISOLATION LEVEL SERIALIZABLE when starting any transaction that involves PREPARE TRANSACTION. Never rely on the server default for distributed transaction workflows.

Monitor pg_prepared_xacts regularly: Orphaned prepared transactions block VACUUM, cause lock contention, and bloat storage. Use the query below as a scheduled health check or monitoring alert:

-- Alert if any prepared transaction is pending for more than 10 minutes
SELECT gid, prepared, owner,
       EXTRACT(EPOCH FROM (NOW() - prepared)) AS pending_seconds
FROM pg_prepared_xacts
WHERE prepared < NOW() - INTERVAL '10 minutes';
Enter fullscreen mode Exit fullscreen mode

Related errors to watch: 25000 (Invalid Transaction State — parent class), 25006 (Read Only SQL Transaction), and 40001 (Serialization Failure — which may appear after switching to SERIALIZABLE and requires proper retry logic in your application).


📖 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)