PostgreSQL Read-Only SQL Transaction (SQLSTATE 25006)

When you execute a write operation inside a read-only transaction or against a read-only server, PostgreSQL raises ERROR: cannot execute <statement> in a read-only transaction with SQLSTATE 25006 and condition name read_only_sql_transaction. The exact message varies by statement type, for example:

ERROR:  cannot execute INSERT in a read-only transaction
ERROR:  cannot execute UPDATE in a read-only transaction
ERROR:  cannot execute DELETE in a read-only transaction
ERROR:  cannot execute CREATE TABLE in a read-only transaction

On a hot standby replica the message is slightly different:

ERROR:  cannot execute INSERT in a read-only transaction
DETAIL:  The transaction has not been granted read-write access because it is a standby server.

What This Error Means

SQLSTATE 25006 belongs to class 25Invalid Transaction State. PostgreSQL enforces transaction access-mode rules at the statement execution level: before running any data-modifying command (DML or DDL), it checks whether the current transaction is in read-only mode. If it is, execution stops immediately and 25006 is raised. The transaction itself remains open but the failing statement is rolled back; you can continue using the transaction for read queries, or roll it back and start a new read-write transaction.

A transaction enters read-only mode in one of two ways: the client explicitly requested it (SET TRANSACTION READ ONLY or BEGIN READ ONLY), or the server itself is in read-only mode — which is the case for physical hot standbys, logical replicas that do not accept writes, or when the server parameter default_transaction_read_only = on is set.

On a hot standby, the restriction is architectural: the replica replays WAL from the primary and cannot accept local writes. PostgreSQL still opens a transaction for the client, assigns a snapshot, and lets you run SELECT queries, but any attempt to modify data triggers 25006.

Common Causes

  1. Explicit READ ONLY transaction mode. A BEGIN READ ONLY or SET TRANSACTION READ ONLY was issued (possibly by a connection pool, ORM, or application layer) before the write statement ran. This is often set intentionally for reporting queries but leaked into a code path that also writes.

  2. default_transaction_read_only = on in postgresql.conf or postgresql.auto.conf. A server-wide setting that makes every new transaction read-only by default. This is occasionally set on analytical replicas or by accident.

  3. Writing to a hot standby replica. The connection string or load-balancer target points to a replica rather than the primary. This is the most common production cause, especially when using read/write splitting proxies (PgBouncer, RDS Proxy, HAProxy) that are misconfigured or route writes to the wrong backend.

  4. Session-level SET default_transaction_read_only = on. A prior statement in the same session (e.g., inside a stored procedure or a framework setup hook) set the session variable to on, making all subsequent transactions read-only until the session ends or the setting is reversed.

  5. pg_restore or pg_dump sessions. Some restore tools open read-only transactions for safety. Writing inside such a session will fail with 25006.

How to Fix read_only_sql_transaction

  1. Check whether the transaction was explicitly opened as read-only and remove that flag.

    -- Instead of:
    BEGIN READ ONLY;
    INSERT INTO orders (customer_id, total) VALUES (42, 99.99);  -- ERROR 25006
    
    -- Use:
    BEGIN;  -- or BEGIN READ WRITE;
    INSERT INTO orders (customer_id, total) VALUES (42, 99.99);
    COMMIT;
    
  2. Check and correct default_transaction_read_only.

    -- Check the server-level setting:
    SHOW default_transaction_read_only;
    
    -- Check the session-level setting:
    SELECT current_setting('default_transaction_read_only');
    
    -- Override for the current session if needed:
    SET default_transaction_read_only = off;
    

    If the server-wide setting is the problem, update postgresql.conf and reload:

    # In postgresql.conf:
    # default_transaction_read_only = off   # (change from on)
    psql -c "SELECT pg_reload_conf();"
    
  3. Verify you are connecting to the primary, not a replica.

    -- Returns true on a standby, false on the primary:
    SELECT pg_is_in_recovery();
    

    If the result is true, your write connection is pointing at a replica. Correct the connection string, DSN, or load-balancer rule to route writes to the primary. In AWS RDS/Aurora, use the writer endpoint; in Patroni/pg_auto_failover clusters, use the primary endpoint or VIP.

  4. Override read-only mode for a specific transaction on an otherwise read-only-defaulted server.

    If default_transaction_read_only = on is intentional server-wide but you need writes in specific cases:

    SET LOCAL default_transaction_read_only = off;
    -- or equivalently, within an explicit transaction:
    BEGIN READ WRITE;
    INSERT INTO ...;
    COMMIT;
    
  5. Check ORM or connection pool configuration. Frameworks like Django, SQLAlchemy, and Rails, as well as proxies like PgBouncer, can be configured to use read-only transaction modes. Check for readonly=True, read_only=True, or equivalent settings in your ORM's database configuration and connection pool setup.

Additional Information

  • Class 25 covers all Invalid Transaction State errors. Related codes include 25001 (active_sql_transaction), 25002 (branch_transaction_already_active), 25P01 (no_active_sql_transaction), and 25P02 (in_failed_sql_transaction).
  • The behavior on hot standbys has been stable since PostgreSQL 9.0, when hot standby was introduced.
  • pg_is_in_recovery() is the canonical function to distinguish primary from standby at runtime.
  • In AWS RDS Multi-AZ and Aurora, failover changes which endpoint is the primary. If your application caches the connection after a failover, it may end up connected to the new standby, causing 25006 on write operations.
  • JDBC drivers expose this as PSQLException with getSQLState() returning "25006". Psycopg2/psycopg3 raises psycopg2.errors.ReadOnlySqlTransaction (a subclass of InternalError).
  • SET TRANSACTION READ ONLY must be called before the first query in the transaction; calling it after any query raises 25001 instead.

Frequently Asked Questions

Why am I getting this error after a failover? After a primary/replica failover, what was the primary becomes a standby. If your application still holds open connections to the old primary (now a standby), any write attempts will fail with 25006. Implement reconnection logic that re-checks pg_is_in_recovery() on error, or use a connection pool or proxy that automatically re-routes writes to the new primary.

Can I write to a PostgreSQL replica at all? No — physical standbys are strictly read-only at the SQL level. Logical replicas that use logical replication (not streaming replication) can in principle accept writes to tables that are not part of the replication set, but this is rarely the right design. If you need to write from multiple nodes, consider a multi-primary solution like Citus or BDR.

How do I find out why my transaction is read-only? Run these queries to narrow it down:

-- Is this a standby?
SELECT pg_is_in_recovery();

-- What is the server default?
SHOW default_transaction_read_only;

-- What is the current session setting?
SELECT current_setting('default_transaction_read_only');

-- Was the current transaction opened READ ONLY?
SELECT access_mode FROM pg_stat_activity WHERE pid = pg_backend_pid();
-- (access_mode column available in PostgreSQL 16+)

Does 25006 roll back my entire transaction? No. The failed statement is rolled back, but the transaction remains open. You can issue a ROLLBACK and start a new read-write transaction, or simply catch the error in application code, roll back, and retry with the correct connection.

Subscribe to the Pulse Newsletter

Get early access to new Pulse features, insightful blogs & exclusive events , webinars, and workshops.

We use cookies to provide an optimized user experience and understand our traffic. To learn more, read our use of cookies; otherwise, please choose 'Accept Cookies' to continue using our website.