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 25 — Invalid 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
Explicit
READ ONLYtransaction mode. ABEGIN READ ONLYorSET TRANSACTION READ ONLYwas 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.default_transaction_read_only = oninpostgresql.conforpostgresql.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.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.
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 toon, making all subsequent transactions read-only until the session ends or the setting is reversed.pg_restoreorpg_dumpsessions. Some restore tools open read-only transactions for safety. Writing inside such a session will fail with25006.
How to Fix read_only_sql_transaction
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;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.confand reload:# In postgresql.conf: # default_transaction_read_only = off # (change from on) psql -c "SELECT pg_reload_conf();"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.Override read-only mode for a specific transaction on an otherwise read-only-defaulted server.
If
default_transaction_read_only = onis 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;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
25covers all Invalid Transaction State errors. Related codes include25001(active_sql_transaction),25002(branch_transaction_already_active),25P01(no_active_sql_transaction), and25P02(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
25006on write operations. - JDBC drivers expose this as
PSQLExceptionwithgetSQLState()returning"25006". Psycopg2/psycopg3 raisespsycopg2.errors.ReadOnlySqlTransaction(a subclass ofInternalError). SET TRANSACTION READ ONLYmust be called before the first query in the transaction; calling it after any query raises25001instead.
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.