PostgreSQL Error 25P01: No Active SQL Transaction
PostgreSQL error 25P01 (no active sql transaction) occurs when you attempt to execute a transaction control command — such as ROLLBACK or COMMIT — without a corresponding active transaction block. Since PostgreSQL operates in autocommit mode by default, each statement is automatically wrapped in its own transaction, meaning an explicit BEGIN is required before issuing ROLLBACK or COMMIT manually.
Top 3 Causes
1. Calling ROLLBACK or COMMIT Without BEGIN
The most common cause is issuing ROLLBACK or COMMIT outside of an explicit transaction block.
-- This will trigger ERROR 25P01
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
ROLLBACK; -- ERROR: there is no transaction in progress
-- Correct approach: wrap with BEGIN
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
2. Mishandled Exception Logic in Application Code
When an exception is thrown and the transaction has already been terminated internally, calling ROLLBACK again results in 25P01. This often happens in ORM frameworks or custom error-handling middleware.
-- Safe pattern: check transaction status before rolling back
SELECT CASE
WHEN pg_current_xact_id_if_assigned() IS NOT NULL
THEN 'active transaction'
ELSE 'no active transaction'
END AS tx_status;
-- Only rollback if a transaction is truly active
BEGIN;
INSERT INTO orders (order_id, total) VALUES (2001, 150.00);
-- If something goes wrong:
ROLLBACK; -- Safe here because BEGIN was called
3. Stale Connection State in Connection Pools
When using connection poolers like PgBouncer, a connection returned to the pool without proper cleanup may carry leftover transaction state. The next client reusing that connection can unexpectedly trigger 25P01.
-- Run this before returning a connection to the pool
DISCARD ALL;
-- Or check for lingering idle transactions
SELECT pid, state, query, now() - state_change AS duration
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes';
-- Force-terminate long-running idle transactions if needed
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '10 minutes';
Quick Fix Solutions
- Always pair
BEGINwithCOMMITorROLLBACKin your scripts. - Use conditional transaction management in automation scripts:
-- Safe migration script pattern
\set ON_ERROR_STOP on
BEGIN;
ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;
UPDATE users SET last_login = created_at WHERE last_login IS NULL;
-- Verify before committing
DO $$
BEGIN
IF (SELECT COUNT(*) FROM users WHERE last_login IS NULL) > 0 THEN
RAISE EXCEPTION 'Migration validation failed: NULL values remain';
END IF;
END;
$$;
COMMIT;
Prevention Tips
Use transaction wrappers in application code. Always manage transaction lifecycle through a dedicated wrapper (e.g., Python's
with conncontext manager, Spring's@Transactional) rather than manually issuingBEGIN/COMMIT/ROLLBACK. This eliminates the risk of orphaned transaction commands entirely.Configure
server_reset_query = DISCARD ALLin PgBouncer. This ensures every connection returned to the pool is fully reset — clearing transaction state, session variables, and temporary objects — before being handed to the next client. Without this, stale transaction state silently propagates across sessions and causes hard-to-debug errors like 25P01.
Related Errors
-
25001 –
active sql transaction: Triggered whenBEGINis called inside an already active transaction. -
25P02 –
in failed sql transaction: Occurs when queries are executed after a transaction has been aborted; requiresROLLBACKto recover. -
40001 –
serialization failure: Can cascade into 25P01 if retry logic improperly callsROLLBACKon an already-terminated transaction.
📖 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)