Skip to main content

PG.L3.MISSING_STATEMENT_TIMEOUT — Bounding how long a step may hold what it took

  • Category: safety
  • Level: 3
  • Confidence: deterministic
  • Downtime class: blocking
  • Stability: stable
  • Suites: lint
  • Applies to: PostgreSQL 18

lock_timeout protects you from a lock you cannot get. statement_timeout protects you from one you did.

Once a strong-lock operation has acquired its ACCESS EXCLUSIVE lock, it holds it for as long as the work takes — and everything else waits, for exactly as long. A table rewrite that was estimated at thirty seconds and turns out to take twenty minutes is not a slow deploy; it is twenty minutes of outage, and nothing stops it on its own.

statement_timeout is what caps the damage a single deploy step can do.

Why an estimate is not a bound​

The two rules exist separately because the failures are separate. Estimates come from the row count you had when you wrote the migration, from a staging database an order of magnitude smaller, from a bigint change you thought was a metadata change. Every one of those is a reasonable belief that can be wrong by a factor of a hundred.

A timeout does not need the estimate to be right. It needs the ceiling to be tolerable.

Statements beyond ALTER TABLE that count​

A trigger, a policy or a rule on an existing table, a view or a sequence that already exists, and most forms of ALTER INDEX take locks that stop reads or writes. Measured on PostgreSQL 18.4: CREATE TRIGGER takes SHARE ROW EXCLUSIVE on its table; DROP TRIGGER, the policy statements and CREATE RULE take ACCESS EXCLUSIVE on it; CREATE OR REPLACE VIEW, ALTER VIEW and DROP VIEW take ACCESS EXCLUSIVE on the view; ALTER SEQUENCE takes SHARE ROW EXCLUSIVE on the sequence; and ALTER INDEX … SET TABLESPACE holds ACCESS EXCLUSIVE on the index while it moves the files. A DROP SCHEMA … CASCADE, and any other DROP that cascades, takes the objects it reaches along with their locks. All of them count. A trigger, policy or rule on a table the same migration creates does not, like any other lock on that table.

Measured and not counted: ALTER INDEX … RENAME and ALTER INDEX … SET (…), COMMENT ON, GRANT and REVOKE, CREATE VIEW, CREATE SEQUENCE, CREATE FUNCTION, CREATE EXTENSION, ALTER TYPE … ADD VALUE, and a DROP SCHEMA without CASCADE, none of which blocks a read or a write.

What does not count as a strong lock​

ALTER TABLE … VALIDATE CONSTRAINT, the second step after NOT VALID, takes SHARE UPDATE EXCLUSIVE. No read and no write waits behind that lock, so a migration that only validates constraints draws no finding. An ALTER TABLE that validates a constraint and does anything else in the same statement takes the stronger lock and still counts.

The same holds for a change of a maintenance storage parameter — fillfactor, toast_tuple_target, parallel_workers and the vacuum and autovacuum family, each also under toast., set or reset. PostgreSQL changes those under SHARE UPDATE EXCLUSIVE too. Any other parameter counts: user_catalog_table, for one, takes ACCESS EXCLUSIVE.

Flagged​

DB::statement('ALTER TABLE orders ADD COLUMN notes text');

Preferred​

DB::statement("SET lock_timeout = '3s'");
DB::statement("SET statement_timeout = '30s'");
DB::statement('ALTER TABLE orders ADD COLUMN notes text');

Set it high enough to be honest​

A statement_timeout shorter than the work genuinely needs turns a long migration into a migration that never completes, retried forever. If a step really does need ten minutes under a lock, the answer is not a ten-minute timeout — it is a different migration strategy (add-backfill-swap), and the timeout stays as the ceiling for when that goes wrong too.

SQLens sets its own, and never yours​

Worth saying plainly on this page: the audit and catalog readers bound their own sessions with these settings before touching anything. What SQLens will not do is change your application's connection — its reader runs on a separate named connection built from a copy of your config, so the session it seals is its own.

Sources​

The fix material this rule carries​

A finding from this rule carries machine-readable fix material, using this sequence:

  • timeout_preamble — Bound how long the statement may wait and how long it may run before it is given up on.

The payload is material for you or an agent to apply. SQLens writes no migration and runs no DDL. See the remediation payload for every field, the placeholder semantics, and the version rules a consumer has to follow.