Incident brief
Read-only SQL transaction
A write was attempted inside a transaction explicitly marked READ ONLY. PostgreSQL blocked the write instead of allowing it.
What lands in your log
ERROR: cannot execute INSERT in a read-only transaction
In 10 seconds
- What triggers it
- BEGIN READ ONLY.
- Fix
- Don't open the transaction as READ ONLY if it needs to write to ordinary tables.
- Proof
- Reproduced on PostgreSQL 16.14 → An INSERT into an ordinary table inside a READ ONLY transaction was rejected with SQLSTATE 25006. No row was added.
Fix
What to do right now
Application-level steps for this error.
- Don't open the transaction as READ ONLY if it needs to write to ordinary tables.
- Writes to temporary tables are allowed even inside a READ ONLY transaction, see the premium test.
- Don't assume SET SESSION CHARACTERISTICS can flip the current transaction to read/write mid-flight, see the counterexample.
BEGIN;
INSERT INTO audit.events VALUES (1);
COMMIT;For this error
See this error live on the server
Run these against the affected instance to confirm the diagnosis before you act.
Read-only SQL transaction depends on the current connection's transaction state. Confirm that state on the same connection before changing application flow.
Read-only settings at session and database level
Read transaction state without inferring it from a pooler's connection state.
SELECT current_setting('transaction_read_only') AS transaction_read_only,
current_setting('default_transaction_read_only') AS default_transaction_read_only;Why it happens
What PostgreSQL is telling you
The mechanism behind the error, grounded in the official manual, not paraphrased.
PostgreSQL 16 Documentation, SET TRANSACTION
When a transaction is read-only, the following SQL commands are disallowed: INSERT, UPDATE, DELETE, MERGE, and COPY FROM if the table they would write to is not a temporary table; all CREATE, ALTER, and DROP commands...Read the full section on postgresql.org →
The blocked INSERT
READ ONLY marks the whole transaction as non-writing. Per the manual, INSERT into a non-temporary table is one of the commands explicitly disallowed in that mode, so PostgreSQL blocked it immediately.Confirming no row was added
audit.events is still empty, the blocked INSERT never took effect.Reproduce & verify
A real, single-session PostgreSQL reproduction
A literal transcript of SQL run against a live PostgreSQL instance in an isolated lab. The commands below are exactly what was executed.
- 1BEGIN READ ONLY.
- 2Try to INSERT into an ordinary (non-temporary) table.
- 3SQLSTATE 25006 is reported; the transaction is aborted.
One client: open a READ ONLY transaction, then try to INSERT into an ordinary table.
CREATE SCHEMA IF NOT EXISTS audit;
DROP TABLE IF EXISTS audit.events;
CREATE TABLE audit.events (id int);BEGIN READ ONLY;
INSERT INTO audit.events VALUES (1);
ROLLBACK;SELECT count(*) FROM audit.events;What PostgreSQL actually returned
NOTICE: table "events" does not exist, skipping
DROP TABLE
CREATE TABLEBEGIN
ERROR: cannot execute INSERT in a read-only transaction
ROLLBACK count
-------
0
(1 row)The manual's disallowed-commands list explicitly excludes temporary tables from the write restriction. This was tested directly: a temp table created before the READ ONLY transaction, written to inside it.
Without this
Above: INSERT into an ordinary table inside READ ONLY is rejected.
With this, tested
Below: INSERT into a pre-existing temp table inside the same READ ONLY mode succeeds.
- A second operational test: exact SQL, raw output, measured result, and engineer notes
- Fix that doesn't retroactively unlock the current transaction: exact SQL, output, and verdict
- A manual-grounded production interpretation of the lab result
Card required. Cancel before day 7 and you are not charged.
Runbook to fix this
The runbook for this incident
Full step-by-step fixes for the condition behind this error: the diagnosis, the exact SQL, and output captured in the lab.
Connected
Everything this error touches
Every page this SQLSTATE connects to: the concept that explains it, the runbooks that fix it, the parameters you tune to prevent it, and the sibling errors it travels with. All real cross-references. Jump straight in, or open the full interactive map.
Fix it — runbooks
Related errors
Verification
- Last verified
- 2026-07-16 (Docker lab, PostgreSQL 16.14)
- Verification scope
- Verified against PostgreSQL 16.14 in an isolated lab environment
- Audit status
- reviewed
Went further?
Pro unlocks the second lab proof
Free page stops the bleeding. Pro adds the operational test, SQLSTATE audit, and deeper evidence, same error, more certainty.