# 25006 — read_only_sql_transaction

> PostgreSQL SQLSTATE 25006: read-only transaction failures, diagnostics, and the transaction boundary needed for repair.
---

# 25006 — read_only_sql_transaction

## At a glance {#at-a-glance}

`25006` is returned when a command would write in a read-only transaction. The selected case issues `CREATE TABLE` after `SET TRANSACTION READ ONLY`, observes `INERROR`, rolls back, and creates the table after leaving the read-only block.

<!-- BEGIN SQLSTATE FACTS: generated by scripts/generate.py; do not edit -->

| Field | Value |
| --- | --- |
| SQLSTATE | `25006` |
| Condition | `read_only_sql_transaction` |
| Status | `active` |
| Known present by | `7.4` |
| Locked snapshots | `9.0.23, 9.1.24, 9.2.24, 9.3.25, 9.4.26, 9.5.25, 9.6.24, 10.23, 11.22, 12.22, 13.23, 14.24, 15.19, 16.15, 17.11, 18.6, 19beta3` |
| Macros | `ERRCODE_READ_ONLY_SQL_TRANSACTION` |
| Aliases | `—` |

<!-- source facts: data/errcodes/25006.json -->
<!-- END SQLSTATE FACTS -->

## Meaning {#meaning}

The code covers several guards. `PreventCommandIfReadOnly` formats the command name into `cannot execute %s in a read-only transaction`; the fixed source also has recovery-only messages for temporary tables and replication origins. The selected case covers the ordinary explicit read-only transaction path, while a standby or recovery path can use a different command-specific message.

## Diagnosis {#diagnosis}

Record the exact command, SQLSTATE, severity, and transaction status. Before retrying, inspect the effective `SHOW transaction_read_only` and, for a server-level boundary, `pg_is_in_recovery()`; also identify whether a pool routed this connection to a standby. In the selected run, `CREATE TABLE` returns 25006 and leaves the explicit block `INERROR`; `ROLLBACK` is required before the same session can be used. A later `CREATE TABLE` outside the read-only block succeeds.

## Response {#response}

Decide whether the operation belongs in a read-only transaction. If it must write, use a read/write transaction on the primary or run it outside the read-only block, and roll back the failed block before returning the connection to a pool. A standby or recovery-only 25006 path requires routing the operation to the primary; retrying the same write on the same target cannot change its access mode.

## Source messages {#messages}

The core utility template is `cannot execute %s in a read-only transaction`. The same utility guard uses `cannot execute %s during recovery` on a recovery target; recovery-only temporary-table and replication-origin paths have their own fixed text. `%s` is the executing command name, so these are not context-free static messages.

## Observed diagnostics {#observed}

`18.6 (Homebrew) / latest`: SQLSTATE `25006`; primary `cannot execute CREATE TABLE in a read-only transaction`; `INERROR → IDLE`; repaired relation count `0`; final `IDLE`.
`10.21 (Debian 10.21-1.pgdg90+1) / pg10`: SQLSTATE `25006`; primary `cannot execute CREATE TABLE in a read-only transaction`; `INERROR → IDLE`; repaired relation count `0`; final `IDLE`.

## Representative case {#case}

The runner reads the setup, read-only transaction, rollback, and outside-block repair statements below from the shared registry; complete assertions, environment, and cleanup are in [case export](../data/cases/25006.json).

<!-- BEGIN SQLSTATE SNIPPET: read_only_transaction -->
```sql
-- create
CREATE TABLE items(id integer PRIMARY KEY, note text NOT NULL);
-- begin
BEGIN;
-- read_only
SET TRANSACTION READ ONLY;
-- trigger
CREATE TABLE blocked(id integer PRIMARY KEY);
-- rollback
ROLLBACK;
-- repair
CREATE TABLE blocked(id integer PRIMARY KEY);
-- verify
SELECT count(*) FROM blocked;
```
<!-- END SQLSTATE SNIPPET -->

The SQLSTATE, diagnostics, states, and repair assertions for this excerpt come from the shared registry (SHA-256 `62db30e401b1f72fa50958d0ac612b2b1eb636299532dd1ad246c167e4f9fadf`); [structured evidence](../data/evidence/25006.json).

Authored evidence IDs: `identity`, `utility-path`, `other-paths`, `runtime`. Selected runtime records: `runtime.25006-batch2-latest-20260909.latest`, `runtime.25006-batch2-pg10-20260909.pg10`.
## Versions and limits {#versions}

The selected `CREATE TABLE` case passes on PostgreSQL 18.6 and 10.21 with the same primary text, `INERROR → IDLE` recovery, and a successful repaired relation. Other 25006 source paths remain outside this runtime case.

## Related {#related}

[25P02 failed SQL transaction](../25p02/), [25001 active SQL transaction](../25001/), [25P03 idle-in-transaction timeout](../25p03/).

## Sources {#sources}

- [`src.errcodes.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/errcodes.txt) (SHA-256 `6e8de346643ba84aa3c9c6a73360acfc7b2dfb89162c06c08ce9bf5bcd5bbcba`)
- `src.calls.REL_18_6` — `raw/calls/REL_18_6.jsonl` (SHA-256 `9ee8a0e81d8f0825c5c1ae45583439859a26e602bdd4ce2f2a62aa278867ccbf`)
- [`src.utility.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/tcop/utility.c#L397-L448) (SHA-256 `7aae5d07628b6debf8456d1d4ea96f28912232192e56ea25773b4c4b61235a00`)
- [`src.namespace.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/catalog/namespace.c#L4418-L4437) (SHA-256 `8c9e6a99e84fa2cec8a9b1de2ecbe3966a13f4ad68c6ccfbfb8134a754d73e56`)
- [`src.origin.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/replication/logical/origin.c#L189-L200) (SHA-256 `81e5d5b4539b67bb372f0f0a05395af900c322cdbcec8a4b1f358333a16e6518`)
- [`doc.set-transaction.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/doc/src/sgml/ref/set_transaction.sgml#L131-L145) (SHA-256 `33554463a2c9da1cf2c72cc27d4647d557204bb13a03cfeccb1b83f237a46589`) · [official documentation](https://www.postgresql.org/docs/18/sql-set-transaction.html)
