# 25P04 — transaction_timeout

> PostgreSQL SQLSTATE 25P04: session termination when an explicit or implicit transaction exceeds transaction_timeout.
---

# 25P04 — transaction_timeout

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

`25P04` is a FATAL condition emitted by `transaction_timeout`, which covers both an explicit `BEGIN` transaction and an implicitly started single-statement transaction. The selected 18.6 case uses 300 ms; PG10 is a source/version preflight skip because the setting is unavailable there, so it does not probe an unknown setting. A separate PG16.15/PG17.11 boundary comparison records unavailable-setting `42704` on PG16 and natural FATAL `25P04` on PG17; it is separate from the selected base case.

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

| Field | Value |
| --- | --- |
| SQLSTATE | `25P04` |
| Condition | `transaction_timeout` |
| Status | `active` |
| Known present by | `17.0` |
| Locked snapshots | `17.11, 18.6, 19beta3` |
| Macros | `ERRCODE_TRANSACTION_TIMEOUT` |
| Aliases | `—` |

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

## Meaning {#meaning}

`transaction_timeout` limits the lifetime of an explicit or implicit transaction, including time spent executing or waiting while that transaction is open. When it fires, PostgreSQL emits `terminating connection due to transaction timeout` and terminates the session. It is different from `statement_timeout`, which aborts one statement, and from `idle_in_transaction_session_timeout`, which covers only client-idle time inside an open transaction. If `transaction_timeout` is shorter than or equal to either of those settings, the longer timeout is ignored; prepared transactions are not subject to it.

## Diagnosis {#diagnosis}

On a supported server, capture the effective `SHOW transaction_timeout`, `SHOW statement_timeout`, and `SHOW idle_in_transaction_session_timeout` values on the timed session itself. A control connection's `SHOW` or `pg_settings` describes the observer backend and cannot establish another session's effective `SET` values. From the control connection inspect `pg_stat_activity` fields such as `pid`, `application_name`, `state`, `xact_start`, `state_change`, `query_start`, and `query` to identify the owning application or pool and whether the transaction is active or idle. Match the collector record by backend PID, SQLSTATE 25P04, `error_severity = FATAL`, and the exact message. In the selected 18.6 run the driver also exposed 25P04 and the original connection was closed; a fresh owner connection successfully ran `SELECT 1`. PG10 is skipped by the minimum-version preflight before `SET transaction_timeout` is sent.

## Response {#response}

Discard the terminated session and reconnect. Keep legitimate transactions within the chosen budget by shortening work, splitting a long unit of work, or raising the effective timeout; do not lower it to hide the error. Set it to `0` only when the deployment deliberately accepts the absence of this guard. When the server closes the terminated session, it rolls back the open incomplete transaction before exiting; the dead connection cannot accept a later `ROLLBACK`. A known transaction that had not committed on that session cannot be recovered through that connection. If a separate network failure left it uncertain whether `COMMIT` reached the server, reconcile the business result from a fresh connection before retrying; the selected case sent no commit and does not establish that uncertainty. PG10 has no compatible setting in this batch.

## FATAL message {#messages}

The server source emits `terminating connection due to transaction timeout` at `FATAL` severity; the original connection is terminated.

## Observed diagnostics {#observed}

`18.6 (Homebrew) / latest`: FATAL SQLSTATE `25P04`; primary `terminating connection due to transaction timeout`; backend PID `30576`; original connection closed `True`; collector CSV `25P04`; collector JSON `25P04`; fresh probe `1`.
`10.21 (Debian 10.21-1.pgdg90+1) / pg10`: case `not_applicable` (minimum-version preflight; the unsupported setting was not probed).

Separate boundary comparison: `16.15 / pg16` returned `42704` for unavailable `transaction_timeout` and remained `IDLE`, so it is not 25P04 coverage. `17.11 / pg17` produced FATAL `25P04` after 300 ms with backend PID `73`; CSV and JSON both recorded FATAL `25P04`, the original connection closed, and a fresh connection was `IDLE`. These records are outside the selected base case.

## Representative case {#case}

The SQL excerpt below is not a paste-all script: run the setting, PID, and `BEGIN` on a test connection, then stop sending it queries while a separate observer or log collector waits for FATAL; after the old connection is terminated, open a fresh owner connection for the final probe. The runner reads these statements from the shared registry; complete assertions, collector correlation, environment, and cleanup are in [case export](../data/cases/25p04.json).

<!-- BEGIN SQLSTATE SNIPPET: transaction_timeout -->
```sql
-- set_timeout
SET transaction_timeout = '300ms';
-- backend_pid
SELECT pg_backend_pid();
-- begin
BEGIN;
-- probe
SELECT 1;
```
<!-- END SQLSTATE SNIPPET -->

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

Authored evidence IDs: `identity`, `timeout-path`, `runtime`, `runtime.version-boundary`. Selected base runtime record: `runtime.25P04-batch2c-latest-20260909.latest`. Separate boundary records: `runtime.25P04-boundary-pg16-20260909.pg16` and `runtime.25P04-boundary-pg17-20260909.pg17`.

The separate boundary comparison has already recorded the availability check: PG16 returned `42704` and PG17 continued to a natural FATAL `25P04`. This boundary block is branched, not a paste-all script. On PG16, run `SHOW transaction_timeout`; after the expected `42704`, use the same still-healthy connection for `SELECT 1` if you need to prove recovery, then stop and skip `SET transaction_timeout`, `BEGIN`, and the timeout wait. On PG17 or later, after `SHOW` succeeds, run `SET transaction_timeout`, capture the PID, and run `BEGIN`; then stop sending queries and let the observer or collector wait for the old connection's FATAL and closure. Open a fresh connection only for the final probe. The boundary records are diagnostic comparisons and do not add to the selected base case count.

<!-- BEGIN SQLSTATE SNIPPET: transaction_timeout_version_boundary -->
```sql
-- availability_probe
SHOW transaction_timeout;
-- set_timeout
SET transaction_timeout = '300ms';
-- backend_pid
SELECT pg_backend_pid();
-- begin
BEGIN;
-- probe
SELECT 1;
```
<!-- END SQLSTATE SNIPPET -->

## Versions and limits {#versions}

The locked catalogue first observes `transaction_timeout` in 17.0. The selected 18.6 case passes with CSV/JSON collector evidence and a fresh-connection probe. The separate PG17.11 boundary case independently records natural FATAL `25P04`, while PG16.15 records unavailable-setting `42704`; neither replaces the selected base run. The PG10 case is a real `not_applicable` minimum-version preflight skip: the handler does not send or probe the unknown setting, so this is not evidence of an earlier boundary.

## Related {#related}

[25P03 idle-in-transaction timeout](../25p03/), [40001 serialization failure](../40001/), [57014 query canceled](../57014/).

## 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.postgres.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/tcop/postgres.c#L3473-L3501) (SHA-256 `9fb62275b1badf94d01ab351337b60410cd9b3ab1fe63fa9f23d6d2185a21061`)
- [`src.config.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/doc/src/sgml/config.sgml#L10252-L10347) (SHA-256 `ffca322c15315ba670733d6b320f23cbba7374db0d18937298ffc07b58799ef6`) · [official documentation](https://www.postgresql.org/docs/18/runtime-config-client.html)
- [`doc.monitoring.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/doc/src/sgml/monitoring.sgml#L706-L875) (SHA-256 `06b147f8d966fe46821871703ffcee8a3449cc3f7de5b64b9177f48569fe96f6`) · [official monitoring documentation](https://www.postgresql.org/docs/18/monitoring-stats.html)
- [`src.system_views.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/catalog/system_views.sql#L604-L605) (SHA-256 `418f4ae7472fe0742e6fbdc7a055581602c5131e082aa6f17e2c7269bf711972`) · [official `pg_settings` documentation](https://www.postgresql.org/docs/18/view-pg-settings.html)
- [`doc.protocol.18.6`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/doc/src/sgml/protocol.sgml#L61-L65) (SHA-256 `745339e07ebbc2bb67d98b258e25772ada644ef26011e3cd6b3fcbb435235f89`) · [official protocol documentation](https://www.postgresql.org/docs/18/protocol.html)
