Basi della sicurezza
Supabase "canceling statement due to statement timeout", spiegato
Supabase "canceling statement due to statement timeout": una query ha superato il limite del suo ruolo, 3 secondi per un visitatore. Guarda la policy RLS.

In breve
- "canceling statement due to statement timeout" su Supabase significa che Postgres ha fermato una query perché è durata più di quanto concede il ruolo che l'ha inviata: 3 secondi per un visitatore non autenticato, 8 per uno autenticato. Di quello che la query stava facendo non resta nulla.
- La stessa query funziona nel SQL Editor perché l'editor lavora con il ruolo postgres, che non ha un limite suo e salta del tutto le tue policy di Row Level Security. La tua app applica ogni policy a ogni riga che legge.
- Sistema la policy prima di toccare il limite: avvolgi le chiamate di funzione in (select …), indicizza la colonna che la policy confronta e nomina il ruolo. Un limite più alto per i visitatori dà a ogni sconosciuto più tempo con il tuo database.
La pagina della tua app carica il suo elenco, la rotellina gira per qualche secondo e poi niente. Nella console del browser, o in un avviso rosso che ha aggiunto il tuo builder, c'è una riga:
canceling statement due to statement timeout
Apri la stessa tabella in Supabase e ci sono tutte le righe. Chiedi al builder con cui hai fatto l'app (Lovable, Bolt, v0, Cursor, Claude Code o Replit) quale query esegue quella pagina, la incolli nel SQL Editor e torna in un attimo.
Il primo consiglio che troverai per "canceling statement due to statement timeout" su Supabase è alzare il limite. Quel limite sta facendo il suo lavoro, e lo condivide ogni visitatore della tua app. Prima di alzare qualsiasi cosa, guarda la policy di Row Level Security di quella tabella, la regola che Postgres controlla prima di consegnare ogni riga. Nelle misure della stessa Supabase, è lì che una query che dovrebbe durare 7 millisecondi arriva a 11 secondi.
Cosa significa "canceling statement due to statement timeout" su Supabase?
Postgres, il database dentro il tuo progetto Supabase, ha fermato la query perché è durata più di quanto concede il ruolo che l'ha inviata, e ha annullato quello che la query aveva fatto.
La frase viene da Postgres stesso, con il codice di errore 57014, che Postgres
chiama query_canceled. Supabase la passa alla tua app senza modifiche, quindi
l'errore che riceve il tuo codice porta code: '57014' e quella frase come
messaggio. Postgres usa lo stesso codice per canceling statement due to user request, un annullamento che qualcuno ha chiesto, per questo la frase ti dice
più del numero.
Immagina il tuo database come una biblioteca, con un banco e un timer da cucina. Ogni volta che qualcuno chiede dei libri, la bibliotecaria imposta il timer prima di andare agli scaffali, e quanto dura dipende da chi chiede. Quando suona, ogni libro torna al suo posto e il visitatore riceve un foglietto con questa frase. Se la richiesta doveva cambiare qualcosa, anche il cambiamento viene rimesso a posto.
Perché la query funziona nel SQL Editor e fallisce nella mia app?
Perché il SQL Editor chiede come un altro visitatore, uno senza un timer suo e senza regole da controllare.
Ogni richiesta arriva a Postgres con un ruolo, un nome che decide cosa la richiesta può fare. La guida di Supabase sui timeout elenca il limite con cui parte ogni ruolo:
| Chi chiede | Ruolo | Tempo concesso |
|---|---|---|
| Un visitatore della tua app non autenticato | anon | 3 secondi |
| Un utente autenticato della tua app | authenticated | 8 secondi |
| Il tuo server, con la chiave segreta | service_role | nessuno suo, quindi valgono gli 8 secondi del ruolo authenticator |
| Tu, nella dashboard e nel SQL Editor | postgres | nessuno suo, con un tetto predefinito di 2 minuti |
Il ruolo dell'editor ha un secondo vantaggio. La guida di Supabase su Row Level
Security dice che su Supabase postgres ha bypassrls, l'attributo di Postgres
che salta ogni policy. Quindi l'editor risponde senza leggere una sola regola.
La richiesta della tua app legge ogni policy della tabella su ogni riga che
guarda, con un timer che suona dopo tre secondi per un visitatore non
autenticato. L'editor ha cronometrato una query diversa da quella che invia la
tua app.
Nella biblioteca tu fai parte del personale. Entri nel deposito senza timer e nessuno ti chiede la tessera. Il visitatore ha tre secondi, e la bibliotecaria controlla le regole su ogni libro lungo la strada.
Trova la query che è stata annullata
Cerca nei log di Postgres del tuo progetto le parole statement timeout, e
annota quale ruolo ha inviato la query.
La guida di Supabase sui timeout dà una query per questo. Nella dashboard, apri Explorer, scegli Run SQL, imposta Logs come origine, scegli un intervallo di tempo che copra l'errore ed esegui questa versione accorciata:
select timestamp,
log_attributes['parsed.user_name'] as user_name,
log_attributes['parsed.query'] as query
from logs
where source = 'postgres_logs'
and match(event_message, 'statement timeout')
order by timestamp desc
limit 100;
Uno user_name uguale ad authenticator significa che la richiesta è passata
dalla Data API, il server da cui passa ogni chiamata supabase.from(…) della
tua app. La tabella dei ruoli della guida indica authenticator come
PostgREST, che è quel server. La colonna query è l'SQL in cui si è
trasformata la richiesta della tua app, e il nome di tabella che compare lì è
la tabella da guardare.
La pagina Query Performance della dashboard è l'altro posto dove si guarda, e la guida avverte che elenca solo le query lente che sono arrivate in fondo. Una query annullata sta nei log.
Se i log mostrano annullamenti su tutte le tabelle insieme, la parte lenta non è una singola policy, e distinguere un disservizio di Supabase da un problema nella tua app è il punto da cui partire.
Cronometrala come la esegue la tua app
Esegui la query con il ruolo della tua app ed explain analyze davanti, e
Postgres stampa quanto ci ha messo e quante righe la policy ha rifiutato.
È il metodo della guida di Supabase sulle prestazioni di Row Level Security,
dentro begin e rollback perché il cambio di ruolo finisca con il blocco.
Copia l'id di un utente vero da Authentication → Users al posto degli zeri:
begin;
set local role authenticated;
set local request.jwt.claims to '{"role":"authenticated", "sub":"00000000-0000-0000-0000-000000000000"}';
explain analyze select * from public.orders;
rollback;
Contano due righe dell'output. Execution Time è il numero da confrontare con
3 e 8 secondi. Rows Removed by Filter conta le righe che Postgres ha letto e
la policy ha rifiutato, e la guida di Supabase la chiama il segnale da tenere
d'occhio: una policy che scarta la maggior parte della tabella a ogni lettura è
una policy la cui colonna di filtro ha bisogno di un indice.
Per cronometrare cosa riceve un visitatore non autenticato, scrivi
set local role anon; e togli la riga dei claims.
La stessa guida suggerisce di eseguire la query una volta con Row Level Security attiva e una volta disattivata, per vedere se il costo è nella policy. Dice anche di farlo solo fuori dalla produzione, perché disattivarla espone ogni riga. Su un'app online, confronta invece la policy con la sua versione riscritta.
Come può una policy costare secondi?
Postgres controlla la policy su ogni riga che la query legge, e una chiamata di funzione nella policy viene eseguita di nuovo per ognuna di quelle righe.
Supabase ha pubblicato delle misure su questo, prese su una tabella di 100.000 righe. Eccone quattro, il modo lento e quello veloce:
| Cosa controlla la policy | Scritta come | Tempo | Riscritta come | Tempo |
|---|---|---|---|---|
| Le righe che appartengono all'utente autenticato | auth.uid() = user_id | 179 ms | (select auth.uid()) = user_id | 9 ms |
| La stessa regola, senza indice sulla colonna | auth.uid() = user_id | 171 ms | la stessa, con un indice su user_id | meno di 0,1 ms |
| Un controllo admin che legge un'altra tabella | is_admin() | 11.000 ms | (select is_admin()) | 7 ms |
| Un ruolo cercato in un'altra tabella | has_role() = role | 178.000 ms | (select has_role()) = role | 12 ms |
Le prime due sono lente e su quella tabella restano comunque sotto i tre secondi. Le ultime due superano qualsiasi limite che abbia una richiesta di un'app. Una funzione helper che legge una seconda tabella, del tipo che si scrive per sapere se qualcuno è admin, viene chiamata una volta per ogni riga della prima, quindi su quella tabella ha letto la seconda 100.000 volte.
Nella biblioteca, quell'helper è la bibliotecaria che va fino all'ufficio
all'ingresso a chiedere se fai parte del personale, una volta per ogni libro
dello scaffale. Avvolgerlo in (select …) è chiedere una volta e scrivere la
risposta sul foglietto.
Il Performance Advisor di Supabase ne coglie una parte. La sua regola chiamata
Auth RLS Initialization Plan segnala una policy che "re-evaluates
current_setting() or auth.is_admin(), resta fuori da quella descrizione, quindi una pagina dell'advisor
senza avvisi non ha controllato i tuoi helper. Leggi tu le policy della tabella
cercando qualsiasi chiamata di funzione che non sia avvolta.
Sistemala in quest'ordine
Avvolgi le chiamate di funzione, indicizza la colonna che la policy confronta, poi nomina il ruolo. Ogni passo è un breve pezzo di SQL che puoi eseguire nell'editor o passare al tuo builder.
1. Avvolgi ogni chiamata di funzione in (select …).
alter policy "read own orders" on public.orders
using ( (select auth.uid()) = user_id );
alter policy mantiene il nome e il ruolo della policy e sostituisce la
condizione. Usa il nome della tua policy e la colonna della tua tabella, e fai
lo stesso con un helper: (select private.is_admin()). Supabase pone una
condizione: funziona solo per un valore che resta uguale da una riga all'altra.
Perché le parentesi fanno la differenza è spiegato in
la guida per attivare Row Level Security,
e un helper scritto per
risolvere un errore di ricorsione infinita
è proprio il tipo che ne ha bisogno.
2. Indicizza la colonna che la policy confronta.
create index orders_user_id_idx on public.orders (user_id);
La guida di Supabase aggiunge un inghippo. Una colonna conta come indicizzata
solo quando è la prima dell'indice, quindi una tabella la cui chiave primaria è
(team_id, user_id) non ha un indice su user_id. Su una tabella grande in cui
si scrive mentre lavori, la documentazione di Postgres dice che un normale
create index blocca le scritture sulla tabella finché non è costruito.
create index concurrently lo costruisce senza quel blocco, e va eseguito come
istruzione a sé, fuori da una transazione.
3. Nomina il ruolo a cui si rivolge la policy. Una policy scritta
to authenticated non viene mai eseguita per un visitatore non autenticato.
Nelle misure di Supabase, una richiesta non autenticata contro una policy senza
to ha impiegato 170 ms, e con to authenticated meno di 0,1 ms.
4. Filtra anche nella richiesta. Una policy funziona come un where
invisibile su ogni query, e la guida di Supabase consiglia di ripeterlo nella
richiesta della tua app, .eq('user_id', userId), perché Postgres può
pianificare una query più veloce. Nelle loro misure questo ha portato una query
da 171 ms a 9.
5. Togli la join da una policy di team. Una policy che confronta ogni riga con una tabella di membri fa quella ricerca riga per riga:
using ( (select auth.uid()) in (
select user_id from team_user where team_user.team_id = projects.team_id
) )
La guida di Supabase la riscrive perché legga i team una volta sola e confronti con quell'elenco:
using ( team_id in (
select team_id from team_user where user_id = (select auth.uid())
) )
Poi esegui di nuovo l'explain analyze di prima e confronta Execution Time e
Rows Removed by Filter con quello che avevi.
Devo aumentare lo statement timeout?
Solo per una query che hai misurato, riscritto e che ti serve ancora, e allora per quella funzione invece che per un ruolo intero.
I limiti sono voluti. La guida di Supabase all'importazione dei dati dice che
servono a distribuire le risorse in modo equo e a evitare che le query lunghe
pesino sull'intero sistema. Il limite di anon lo condividono tutti i
visitatori non autenticati, e chiunque può inviare quella richiesta con la
chiave pubblica che viaggia nella tua app. Portarlo da tre a trenta secondi dà
a ognuna di quelle richieste trenta secondi del tuo database.
Quando un lavoro richiede davvero più tempo, dai il limite alla funzione che lo
esegue. La guida di Supabase lo descrive per le funzioni ricorrenti che hanno
bisogno di un'eccezione sulla durata, e funziona per una funzione che la tua
app chiama con supabase.rpc(…):
alter function public.monthly_totals() set statement_timeout to '20s';
Se cambi comunque un ruolo, i comandi della guida sono
alter role authenticated set statement_timeout = '15s'; e poi
notify pgrst, 'reload config';, perché la Data API recepisca il nuovo valore.
Supabase limita a 60 secondi le query delle sue librerie client, qualunque cosa
imposti.
Due rimedi che fermano il messaggio e aprono la tabella
Disattivare Row Level Security sulla tabella, e inviare la richiesta con la chiave segreta. Entrambi rendono la query veloce perché entrambi saltano la policy, ed entrambi consegnano le righe a persone che non avrebbero mai dovuto averle.
Disattivarla. Senza policy da eseguire, non si controlla nulla riga per
riga e il timeout sparisce. Supabase stampa il proprio avvertimento accanto al
test che lo suggerisce: disattivare Row Level Security espone ogni riga della
tabella a qualsiasi ruolo con il permesso corrispondente. La sua guida dice
anche che, nei progetti esistenti, una nuova tabella in public parte con
select, insert, update e delete concessi ad anon, il ruolo di un
visitatore non autenticato. Tolta la policy, quei permessi sono tutto ciò che
c'è tra quel visitatore e la tabella.
La chiave segreta. La guida di Supabase dice che anche il ruolo
service_role dietro di essa ha bypassrls, quindi non gira nessuna policy.
Quella chiave sta su un server, e una inviata dal browser è una chiave che
chiunque può leggere dal tuo sito;
quali chiavi sono sicure in un frontend
passa in rassegna la coppia. Non fa nemmeno guadagnare molto tempo, perché la
guida dà a service_role gli stessi 8 secondi di authenticator.
Se una delle tue tabelle risponde a una richiesta senza alcun accesso è uno dei nove controlli della scansione gratuita di Reeve. Legge il tuo sito online come lo vede un visitatore, ci mette circa 20 secondi e non richiede un account.
Mantenere la policy veloce e la tabella chiusa
Una policy che hai sistemato oggi avrà una versione successiva. Il tuo builder scrive policy ogni volta che una funzionalità ne ha bisogno, quindi la prossima può tornare a chiamare una funzione per riga, o aprire la tabella per far caricare una schermata.
Reeve Monitor controlla la tua app da fuori, ogni ora:
- tutti e nove i controlli ogni ora, compreso se una tabella risponde a una richiesta senza accesso
- se l’app è raggiungibile, ogni 60 secondi
- un messaggio quando un risultato cambia, così una tabella che inizia a rispondere agli sconosciuti non aspetta che tu guardi
- un rapporto mensile di quello che ha visto
Reeve Care tiene una copia del tuo database Supabase di prima della tua modifica:
- una copia cifrata secondo una pianificazione, conservata sullo storage di Reeve nell'UE, fuori dal tuo account Supabase
- ogni copia verificata prima di contare, contando le righe di ogni tabella
- un ripristino in un clic che salva quello che c'è adesso prima di sostituirlo
- anche i tuoi file caricati, appena colleghi una credenziale di Storage
- tutto quello che fa Monitor
Cosa fare oggi
Cosa fare
- Cerca
statement timeoutnei log di Postgres e annota la query e il ruolo che l'ha inviata. - Cronometra quella query con
explain analyzecomeanonoauthenticated, i ruoli che usa la tua app, e leggiExecution TimeeRows Removed by Filter. - Avvolgi in
(select …)ogni chiamata di funzione nelle policy di quella tabella, compresi i tuoi helper che il Performance Advisor non segnala. - Indicizza la colonna che confronta ogni policy, e aggiungi
to authenticateda ogni policy pensata solo per utenti autenticati. - Alza un limite per ultimo, sull'unica funzione che ne ha bisogno, e lascia attiva Row Level Security.
Comincia dalla tabella dietro la pagina che non ha caricato. Se non ha ancora policy sue, attivare Row Level Security su ogni tabella ha le tre forme di cui hanno bisogno quasi tutte le tabelle.
FAQ
Cosa significa "canceling statement due to statement timeout" su Supabase?
Postgres ha fermato la tua query perché è durata più di quanto concede il ruolo che l'ha inviata, e ha annullato quello che aveva fatto. Supabase dà 3 secondi a un visitatore non autenticato e 8 a un utente autenticato. L'errore porta il codice 57014, che Postgres usa anche per una query annullata su richiesta, quindi leggi la frase oltre al numero.
Perché la mia query funziona nel SQL Editor ma va in timeout nella mia app?
Perché il SQL Editor lavora con il ruolo postgres e la tua app no. Supabase non dà a postgres un limite suo, e il ruolo ha bypassrls, quindi l'editor non esegue mai le tue policy di Row Level Security. La tua app gira come anon o authenticated, con un limite di 3 o 8 secondi e ogni policy controllata su ogni riga che la query legge.
Row Level Security può causare uno statement timeout?
Sì. Postgres controlla la policy su ogni riga che la query legge, e una chiamata di funzione nella policy viene eseguita di nuovo per ciascuna. In misure pubblicate da Supabase su una tabella di 100.000 righe, una policy che chiamava un helper is_admin() ha impiegato 11.000 millisecondi, e la stessa policy con la chiamata avvolta come (select is_admin()) ne ha impiegati 7.
Come aumento lo statement timeout su Supabase?
Per un singolo lavoro lento, impostalo sulla funzione che lo esegue con alter function … set statement_timeout. Per un intero ruolo, esegui alter role con il nuovo statement_timeout e poi notify pgrst, 'reload config' perché la Data API lo recepisca. Supabase limita a 60 secondi le query delle sue librerie client. Sistema prima la policy, perché il limite di anon lo condividono tutti i visitatori non autenticati.
Devo disattivare RLS per rendere la query più veloce?
No. La query diventa più veloce perché non gira nessuna policy, e la tabella resta aperta a qualsiasi ruolo con un permesso su di essa. La guida di Supabase dice che, nei progetti esistenti, una nuova tabella in public parte con select, insert, update e delete concessi ad anon, il ruolo di un visitatore non autenticato. Avvolgi invece le chiamate di funzione e aggiungi un indice.