PostgreSQL · advanced
PostgreSQL statement timeout: locate the waiting phase
Diagnose PostgreSQL statement_timeout by locating the precise phase in which a query is cancelled: parsing/planning, executor wait on a lock or I/O, executor active run time, or client-side abort. Walks from the SQLSTATE 57014 surface through pg_stat_activity, pg_locks, and wait-event analysis to a targeted remediation that distinguishes server enforcement from client/network cancellation.
The symptoms
- •Application receives SQLSTATE 57014 query_canceled with a message naming statement_timeout, while the same query plan finishes when run interactively or with timeout disabled.
- •psql sessions show ERROR: canceling statement due to statement timeout after roughly the configured millisecond budget, even when the workload is reportedly light.
- •Application log lines record a successful prepare/plan but a later execute step that returns 57014, suggesting the timeout fires after planning rather than at parse.
- •Statement_timeout GUC reports a non-zero value at session, user, database, or server level, but the application layer insists it never sets the parameter.
- •Failures correlate with lock waits, autovacuum, or extension maintenance work; cancelling happens while the backend is reported as waiting, not active.
- •Intermittent cancellations only on specific connection pool members or pooled sessions, while direct sessions from the same user succeed.
Likely causes
- •Server-side statement_timeout inherited from postgresql.conf, ALTER ROLE/DATABASE, or a connection-pooler "session" GUC, applied even when the application does not request it.
- •Per-session override via SET statement_timeout or a SET LOCAL inside a transaction that outlives the intended scope and leaks through a pooled connection.
- •Lock contention on a relation, extension, or advisory lock where the backend spends most of its budget in wait state, triggering the timeout before the executor runs.
- •I/O-bound work: sequential scan, vacuum, ANALYZE, or extension maintenance that exceeds the budget during executor execution rather than during a wait.
- •Client-side or proxy timeout (PgBouncer, JDBC socketTimeout, application HTTP timeout) that surfaces as 57014 because the driver or proxy closed the socket and the backend reported cancellation.
- •idle_in_transaction_session_timeout or transaction_read_only interactions causing the backend to abort a session that the application interpreted as a query timeout.
First ten minutes
- 01Capture the exact SQLSTATE (57014), the ERROR message text, the client timestamp, and the connection identifier (pid, application_name, usename, datname) from the application log or psql terminal.
- 02From a superuser session, query pg_stat_activity for the affected backend: record state, wait_event_type, wait_event, query_start, state_change, xact_start, and the literal statement text.
- 03Resolve the effective statement_timeout for the cancelling backend by checking current_setting('statement_timeout') and inspecting SET search_path; rule out SET LOCAL leakage from a prior transaction in a pool.
- 04Cross-reference pg_locks for the same pid to determine whether the backend was holding locks, waiting on a lock (granted = false), or blocked on an advisory or extension lock at the moment of cancellation.
- 05Inspect the wait_event taxonomy: Lock, LWLock, IO, Activity, Extension, Client, IPC, Timeout categories from pg_stat_activity map the cancellation phase to waits versus active execution.
- 06Differentiate server enforcement from client cancellation by recording backend start timestamp, client connection endpoint if logged, and matching the SQLSTATE origin against server logs versus application/proxy logs.
- 07Note the isolation level, transaction status (idle, idle in transaction, active), and any SET commands that appear in pg_stat_activity backend xact log to scope the timeout origin.
Evidence to collect
- •pg_stat_activity row: pid, datname, usename, application_name, client_addr, state, wait_event_type, wait_event, query_start, state_change, xact_start, backend_xmin, backend_xid.
- •Effective GUC snapshot: current_setting('statement_timeout'), current_setting('idle_in_transaction_session_timeout'), current_setting('lock_timeout'), current_setting('log_min_duration_statement').
- •Lock map: pg_locks rows filtered to the pid, showing locktype, relation, mode, granted, and any blocker pid referenced via blocking_pids.
- •Server log entries containing SQLSTATE 57014 with the associated session line, including log_line_prefix fields such as application_name, session_id, and command tag if log_lock_waits is on.
- •Wait-event distribution over a sample window using pg_stat_activity or cumulative counters to confirm whether the cancellation occurred during Lock/IO/Extension waits versus active execution.
- •Client-side log line showing the same cancellation, including driver-level socket timeout setting (for example socketTimeout) and the timestamp delta versus the server's query_start.
Where to look
- •Server boundary: PostgreSQL configuration files (postgresql.conf, conf.d), ALTER ROLE/DATABASE defaults, and pg_db_role_setting for non-default statement_timeout values.
- •Session boundary: pg_stat_activity, pg_locks, pg_settings, and the SET chain captured in the backend's xact command log (pg_stat_statements if enabled) for per-session overrides.
- •Client boundary: connection pooler (PgBouncer, pgpool) "server_lifetime", "query_wait_timeout" and pool-mode-related behavior, JDBC socketTimeout, ODBC connect_timeout, application HTTP client timeouts.
- •Network boundary: load balancer or proxy idle/connection timeouts that can sever the socket and be reported by the backend as a cancelled statement upon next server interaction.
- •Workload boundary: autovacuum, ANALYZE, maintenance operations, and extension workers (for example pg_stat_statements reset) that contend for the same relations or locks as the cancelled statement.
- •Logical replication boundary: subscriptions and apply workers whose transaction time can be bounded by a session-level statement_timeout that surfaces on the publisher-facing sessions.
Diagnostic steps
- 01Read the SQLSTATE and message: 57014 with the statement_timeout phrase confirms server enforcement; 57014 without it, or a different SQLSTATE with the same wording, signals client-side or proxy cancellation.
- 02Resolve effective statement_timeout with current_setting('statement_timeout') on the cancelling pid and trace it to postgresql.conf, ALTER ROLE/DATABASE, ALTER SYSTEM, or SET; record each layer.
- 03Sample pg_stat_activity for the pid over a short window to classify the wait_event_type at the cancellation point: Lock/LWLock points to blocking, IO to disk-bound execution, Extension to extension work, Client to network.
- 04Correlate pg_locks with pg_stat_activity: if granted = false for a blocking locktype, the cancel happened while waiting; if granted = true and the wait_event is IO or Activity, it fired during active execution.
- 05Inspect pg_settings for reset_val and source columns to identify whether the timeout is a default, a session override, a database/role override, or a file-level setting; this distinguishes transient leaks from persistent misconfiguration.
- 06Compare server log timestamps against application log timestamps for the same session_id; a delta suggests client or proxy cancellation preceded the server's reported cancel.
- 07Reproduce with a controlled timeout using SET statement_timeout to a known value and EXPLAIN (ANALYZE, BUFFERS) on the failing query to observe the exact phase that consumes the budget.
Common mistakes
- •Assuming statement_timeout fires only during active execution; it fires during any backend state including lock and I/O waits, which is the most common source of surprise.
- •Blame the application without checking postgresql.conf, ALTER ROLE, or ALTER DATABASE; server-level defaults silently apply to every connection.
- •Confusing log_min_duration_statement with statement_timeout; a slow log entry is independent of a 57014 cancel and does not prove the timeout was the cause.
- •Reading SQLSTATE 57014 as always server-side; PgBouncer query_wait_timeout, JDBC socketTimeout, and HTTP client timeouts can produce the same client-visible error string.
- •Changing statement_timeout to a very large value as a first response, which masks lock contention and I/O regressions without identifying the waiting phase.
- •Ignoring pool-mode semantics: in transaction or statement pooling, per-session SET LOCAL can leak or be ignored, producing non-deterministic cancellation behavior.
Safe fixes
- •If the wait_event_type is Lock and pg_locks shows granted = false on a long-held relation lock, target the holder's transaction (terminate the blocking pid or shorten its work) rather than raising statement_timeout; use pg_terminate_backend(<pid>) only after confirming the holder's identity and impact.
- •If the cancellation is server-enforced and inherited from a global setting, scope the override to the affected role/database with ALTER ROLE <role> SET statement_timeout TO <ms> or ALTER DATABASE <db> SET statement_timeout TO <ms>, and document the rationale.
- •If the timeout fires during active executor work (wait_event_type IO or Activity with granted locks), narrow the budget using EXPLAIN (ANALYZE, BUFFERS) findings: add an index, raise work_mem for a hash join, or split a batch into smaller statements.
- •If the timeout is client- or proxy-driven, raise the driver socketTimeout or the pooler query_wait_timeout, and align it above the server's statement_timeout so the server is the source of truth for cancellation.
- •If SET LOCAL is leaking through a pooled connection, move the override to a transaction-scoped SET LOCAL inside a single statement or to a session-scoped SET applied by the pooler only on the appropriate pool-mode transactions.
- •If the failure is workload-specific, gate the override on a per-application connection that sets statement_timeout = 0 for known long-running migrations and reports, and keep the global default tight for general traffic.
Prove the fix
- 01Repeat the original workload with the diagnostic queries attached; observe no SQLSTATE 57014 in server logs over a representative window that includes peak contention.
- 02Capture pg_stat_activity snapshots before and after the change: wait_event_type shifts away from Lock, or the cancelling backend no longer appears with a non-zero wait time at the moment of completion.
- 03Record current_setting('statement_timeout') for the previously affected pid and confirm the value matches the intended layer (server, role, database, or session) using pg_settings.source.
- 04Run EXPLAIN (ANALYZE, BUFFERS) on the previously failing query and confirm the total execution time sits comfortably below the configured timeout with realistic concurrency.
- 05Run a regression check that intentionally injects a smaller statement_timeout and verifies the application receives a clean 57014 with the correct cancel phase reported; this proves the boundary remains observable.
- 06Monitor pg_locks for the workload's pids over the next maintenance cycle (autovacuum, ANALYZE) to confirm cancellations do not reappear during routine contention.
Prevention and next steps
- •Document statement_timeout as a layered setting: default, role-level, database-level, and per-application session, with the source of truth recorded in version control alongside postgresql.conf.
- •Standardize connection pooler behavior: disallow SET LOCAL for statement_timeout in transaction-pooled modes, and set the pooler's query_wait_timeout above the server's statement_timeout.
- •Adopt a release checklist that requires EXPLAIN (ANALYZE, BUFFERS) review for any query whose expected runtime is within a factor of the configured statement_timeout.
- •Alert on SQLSTATE 57014 rate per minute per application_name so a regression in the waiting phase is detected before user-facing incidents.
- •Periodically audit pg_db_role_setting and pg_settings for unexpected statement_timeout overrides that may have been introduced during incident remediation.
Safe commands and checks
psql -h <host> -p <port> -U <user> -d <db> -c "SELECT pid, datname, usename, application_name, state, wait_event_type, wait_event, query_start, state_change, xact_start, left(query, 200) AS query FROM pg_stat_activity WHERE state <> 'idle';"
psql -h <host> -p <port> -U <user> -d <db> -c "SELECT pid, current_setting('statement_timeout') AS statement_timeout, current_setting('idle_in_transaction_session_timeout') AS its_timeout, current_setting('lock_timeout') AS lock_timeout FROM pg_stat_activity WHERE pid = <pid>;"
psql -h <host> -p <port> -U <user> -d <db> -c "SELECT locktype, mode, granted, relation::regclass AS relation, pid, pg_blocking_pids(pid) AS blocking_pids FROM pg_locks WHERE pid = <pid> OR pid = ANY(pg_blocking_pids(<pid>::int)) ORDER BY granted DESC, pid;"
psql -h <host> -p <port> -U <user> -d <db> -c "SELECT name, setting, unit, source, category FROM pg_settings WHERE name IN ('statement_timeout','idle_in_transaction_session_timeout','lock_timeout','log_min_duration_statement');"
psql -h <host> -p <port> -U <user> -d <db> -c "SELECT setdatabase, setrole, setconfig FROM pg_db_role_setting WHERE setconfig::text ILIKE '%statement_timeout%';"
psql -h <host> -p <port> -U <user> -d <db> -c "SET statement_timeout = '5s'; EXPLAIN (ANALYZE, BUFFERS) <query>;"
psql -h <host> -p <port> -U <user> -d <db> -c "SELECT count(*) AS cancelled_last_min FROM pg_stat_activity WHERE state = 'idle' AND query LIKE '%canceling statement%';"