Now-Next

SaaS architecture

canceling statement due to statement timeout in PostgreSQL

PostgreSQL's statement timeout error, measured on 12 to 17: where the limit comes from, what it cancels, and how to tell it apart from the other 57014.

Chris van Eijk, Now-Next · · 8 min read

ERROR: canceling statement due to statement timeout (SQLSTATE 57014) means one statement ran longer than statement_timeout allows, and PostgreSQL cancelled it. The time counts everything the statement did: working, but also waiting for a lock. Most statements leave nothing behind, but not all of them (see CREATE INDEX CONCURRENTLY and VACUUM below), and if it ran inside a transaction, that transaction is now aborted. If you never set the limit yourself, it came from the role, the database, the configuration file or the connection string.

We measured on PostgreSQL 17.11, and repeated the cases where the documentation names a version difference on 12.22, 13.23 and 16.15, all in containers.

What happens to the transaction after a statement timeout?

It is aborted, exactly as after any other error:

set statement_timeout = '1s';
begin;
insert into t values (10, 10);
select pg_sleep(3);
-- ERROR:  57014: canceling statement due to statement timeout   (after 1,000.2 ms)
select 1;
-- ERROR:  25P02: current transaction is aborted, commands ignored until end of transaction block
commit;
-- ROLLBACK   (row 10 is gone)

Code that catches the timeout and carries on keeps failing with 25P02 until it rolls back, and a commit() at that point quietly loses the transaction’s earlier work. That trap, per driver, is in current transaction is aborted. The server log had the statement too, without any extra settings: ERROR: canceling statement due to statement timeout followed by STATEMENT: select pg_sleep(3);.

Where does the statement_timeout value come from?

The default is 0, no limit. If you get this error without having set anything, something else set it. pg_settings names the source. We set a different value at each level, one after the other, and opened a new session after each step:

Set withValuesource in pg_settings
nothing0default
alter system set … + pg_reload_conf() (or postgresql.conf)50sconfiguration file
alter database appdb set …40sdatabase
alter role postgres set …30suser
alter role postgres in database appdb set …20sdatabase user
PGOPTIONS='-c statement_timeout=10s'10sclient
set statement_timeout = '5s'5ssession
set local statement_timeout = '2s' in a transaction2ssession; after COMMIT back to 5s

Each row beat all rows above it. So:

select setting, source from pg_settings where name = 'statement_timeout';

tells you where to look. alter role app set statement_timeout = '7s' did not change an open session of app: it still read 50000, configuration file; only a new session got 7000, user. An ALTER SYSTEM plus pg_reload_conf() did reach an open session whose value came from the configuration file (35 s became 25 s), but not one whose value came from its role. After an ALTER ROLE, pooled connections keep the old limit until they reconnect. A role’s setting applies at login only: after set role to a role with statement_timeout = '1s', our session still read 0, default. And the documentation advises against the first row: “Setting statement_timeout in postgresql.conf is not recommended because it would affect all sessions.”

Does waiting for a lock count toward statement_timeout?

Yes. Another session held row 1 (an uncommitted UPDATE, confirmed in pg_locks), and ours ran update t set v = 2 where id = 1:

Our settingsResult
statement_timeout = '2s', no lock_timeoutstatement timeout (57014) after 2,000.3 ms, CONTEXT: while updating tuple (0,1) in relation "t"
both 2sstatement timeout after 2,000.3 ms

A CONTEXT line naming a tuple means the statement spent its time waiting, not computing: look for the blocker, not a missing index. At equal values the statement timeout won. With both set, the lower one fires first; how the two interact, why migrations want lock_timeout, and how to find the session holding the lock, is in canceling statement due to lock timeout.

Is the limit per statement or per query string?

Per statement, since PostgreSQL 13. With statement_timeout = 2s, psql -c 'select pg_sleep(1.5); select pg_sleep(1.5)' sends both statements in one simple-query message:

VersionResult
12.22canceling statement due to statement timeout after 2.03 s, during the second sleep
13.23, 16.15, 17.11both statements succeeded, 3.03 to 3.04 s in total

The PostgreSQL 17 documentation: “If multiple SQL statements appear in a single simple-query message, the timeout is applied to each statement separately. (PostgreSQL versions before 13 usually treated the timeout as applying to the whole query string.)” That is about one simple-query string; for drivers on the extended protocol the documentation says the timer starts at the first query message and is “canceled by completion of an Execute or Sync message”. We did not measure pipelines.

Does statement_timeout also stop CREATE INDEX, COPY and VACUUM?

Yes, all of them. On a table with 5 million rows, where create index took 7.4 s without a limit:

Command, under the limitResult
create index big_v on big(v), 2 scancelled after 2,019.8 ms, no index left
copy big2 from '/tmp/big.csv', 2 scancelled after 2,000.3 ms with CONTEXT: COPY big2, line 4411364, and big2 had 0 rows
vacuum big with 2.5 million dead rows, 500 mscancelled after 500.3 ms, CONTEXT: while scanning block 38090 of relation "public.big"; n_dead_tup still said 2,500,000, but pgstattuple counted 842,998 dead tuples in a repeat run: the vacuum had cleaned part of the table, and the statistics are only reported when it finishes
create index concurrently big_v_cic …, 2 scancelled after 2,025 ms, and the index stayed: indisvalid = f, indisready = f, \d big shows INVALID

The last row is the one that bites. Retrying gave ERROR: relation "big_v_cic" already exists (42P07), and with if not exists it said NOTICE: … already exists, skipping, CREATE INDEX, and the index was still invalid. Drop it (drop index concurrently big_v_cic) before you retry.

How do you allow one slow statement without raising the limit?

With SET LOCAL in a transaction. The session had statement_timeout = '2s':

begin;
set local statement_timeout = '30s';
create index big_v on big(v);   -- CREATE INDEX, 7,415.7 ms
commit;
show statement_timeout;         -- 2s

The raised limit ended with the transaction. CREATE INDEX CONCURRENTLY cannot do this: inside begin it fails with 25001: CREATE INDEX CONCURRENTLY cannot run inside a transaction block. For that one, set statement_timeout = 0, build, reset statement_timeout, in the same session (2,048 ms, indisvalid = t). Do not do a plain SET like that through a transaction-mode pooler: it stays on the server connection for the next client. We measured that leak for lock_timeout behind PgBouncer; the mechanism is in PgBouncer and row-level security.

A SET clause on a function does not set the timer. Inside a function declared with set statement_timeout = '1s', current_setting read 1s, but the timer had already started: called without a limit, it ran its full 3 s, and one declared with 10s around a pg_sleep(3) was still cancelled by a 2 s session limit after 2,000.3 ms (CONTEXT: SQL function "slow_long" statement 1). What counts is the limit in force when the statement arrives; even select set_config('statement_timeout', '1s', false), pg_sleep(2) ran 2 s.

Supabase: which timeout applies?

The Supabase timeouts page lists these default role timeouts: “anon: 3s”, “authenticated: 8s”, “service_role: none (defaults to the authenticator role’s 8s timeout if unset)” and “postgres: none (capped by default global timeout to be 2min)”. A session-level SET, it says, cannot be used “with the Supabase Client API, nor with Supavisor in Transaction mode (port 6543)”. It also offers a SET clause on a function, which “works with the Database REST API when called from the Supabase client libraries”; That works because PostgREST reads the function’s setting and applies it to the transaction before the call, which is also how the anon and authenticated limits reach a request (PostgREST documentation, ‘Hoisted Function Settings’ and ‘Impersonated Role Settings’). On plain PostgreSQL neither happens, as measured above. The page also says: “Dashboard and Client queries have a max-configurable timeout of 60 seconds.” We did not test Supabase or PostgREST.

Is it a statement timeout, or something else?

The same SQLSTATE 57014 has a second text: canceling statement due to user request. pg_cancel_backend() produced it, and so did pgjdbc 42.7.13’s client-side setQueryTimeout(1), after 1,004 ms; with options=-c statement_timeout=2000 in the URL, the same driver threw a PSQLException with the statement-timeout text and SQLState 57014 after 2,003 ms. Two other timeouts end the connection, idle_in_transaction_session_timeout (25P03) and, from 17, transaction_timeout (25P04): with transaction_timeout = '2s', a transaction of a 1 s and a 3 s sleep lost its connection 2.04 s after psql started. On 16.15, set transaction_timeout gave 42704: unrecognized configuration parameter "transaction_timeout".

From the message you see to what set it off, as measured on PostgreSQL 17.11

What exactly does the error say?

  • canceling statement due to statement timeout (57014)statement_timeoutCheck source in pg_settings; a CONTEXT naming a tuple means it was waiting for a lock.
  • canceling statement due to user request (57014)Someone cancelled itpg_cancel_backend(), or a timer in the client, such as JDBC setQueryTimeout.
  • canceling statement due to lock timeout (55P03)lock_timeoutOnly the lock wait counted.
  • FATAL: terminating connection due to idle-in-transaction timeout (25P03)idle_in_transaction_session_timeoutThe connection is gone; the next statement found it closed.
  • FATAL: terminating connection due to transaction timeout (25P04)transaction_timeout (17; not in 16)The whole transaction counts; a lone statement without BEGIN was terminated too.

Each message measured on PostgreSQL 17.11; the two FATAL lines also close the connection.

The FATAL cases differ in kind: your connection pool gets a dead connection, not an error on a live one. What an open, idle transaction holds while it waits for that timeout is in idle in transaction in PostgreSQL.

What we did not measure

Supabase, PostgREST and Supavisor themselves; the Supabase lines above are quoted from their page. Go’s pq, psycopg and DBeaver: whatever prefix they add, the message and 57014 come from the server. Autovacuum, which we did not test under a statement_timeout.

Sources: PostgreSQL 17 documentation on client connection defaults (statement_timeout), the Supabase timeouts guide and the PostgREST documentation on transactions, all read on 22 October 2026. All measurements are our own, on PostgreSQL 17.11, 16.15, 13.23 and 12.22 in containers, with pgjdbc 42.7.13, on 22 October 2026.

Let's talk

What are we building?

One conversation is enough to know whether we fit. Tell us what you have in mind — we will tell you how fast it can happen, and whether we are the right people for it.

Start the conversation