Bases de la sécurité
Supabase "canceling statement due to statement timeout" expliqué
Supabase "canceling statement due to statement timeout" : une requête a dépassé la limite de son rôle, 3 secondes pour un visiteur. Vérifiez la règle RLS.

En bref
- "canceling statement due to statement timeout" sur Supabase signifie que Postgres a arrêté une requête parce qu'elle a duré plus longtemps que ne le permet le rôle qui l'a envoyée : 3 secondes pour un visiteur non connecté, 8 pour un visiteur connecté. Rien de ce que faisait la requête n'est conservé.
- La même requête passe dans le SQL Editor parce que l'éditeur travaille avec le rôle postgres, qui n'a pas de limite propre et ignore complètement vos règles Row Level Security. Votre app applique chaque règle à chaque ligne qu'elle lit.
- Corrigez la règle avant de toucher à la limite : enveloppez les appels de fonction dans (select …), indexez la colonne que la règle compare et nommez le rôle. Une limite plus haute pour les visiteurs donne à chaque inconnu plus de temps avec votre base.
La page de votre app charge sa liste, l'indicateur tourne quelques secondes, puis rien. Dans la console du navigateur, ou dans un message rouge ajouté par votre builder, il y a une ligne :
canceling statement due to statement timeout
Vous ouvrez la même table dans Supabase et toutes les lignes sont là. Vous demandez au builder avec lequel vous avez fait l'app (Lovable, Bolt, v0, Cursor, Claude Code ou Replit) quelle requête envoie cette page, vous la collez dans le SQL Editor, et elle revient en un instant.
Le premier conseil que vous trouverez pour "canceling statement due to statement timeout" sur Supabase est de relever la limite. Cette limite fait son travail, et chaque visiteur de votre app la partage. Avant de relever quoi que ce soit, regardez la règle Row Level Security de cette table, celle que Postgres vérifie avant de rendre chaque ligne. Dans les mesures de Supabase elle-même, c'est là qu'une requête qui devrait prendre 7 millisecondes en passe 11 secondes.
Que veut dire "canceling statement due to statement timeout" sur Supabase ?
Postgres, la base de données de votre projet Supabase, a arrêté la requête parce qu'elle a duré plus longtemps que ne le permet le rôle qui l'a envoyée, et a annulé ce que la requête avait fait.
La phrase vient de Postgres lui-même, avec le code d'erreur 57014, que
Postgres appelle query_canceled. Supabase la transmet telle quelle à votre
app, donc l'erreur que reçoit votre code porte code: '57014' et cette phrase
comme message. Postgres utilise le même code pour canceling statement due to user request, une annulation que quelqu'un a demandée, donc la phrase vous en
dit plus que le numéro.
Imaginez votre base de données comme une bibliothèque, avec un comptoir et un minuteur de cuisine. Chaque fois que quelqu'un demande des livres, la bibliothécaire règle le minuteur avant d'aller vers les rayons, et sa durée dépend de qui demande. Quand il sonne, chaque livre retourne sur son rayon et le visiteur reçoit un papier portant cette phrase. Si la demande devait modifier quelque chose, la modification est remise en place elle aussi.
Pourquoi la requête marche dans le SQL Editor et échoue dans mon app ?
Parce que le SQL Editor demande en tant qu'autre visiteur, un visiteur sans minuteur à lui et sans règles à vérifier.
Chaque demande arrive à Postgres sous un rôle, un nom qui décide de ce que la demande peut faire. Le guide de Supabase sur les timeouts donne la limite avec laquelle chaque rôle démarre :
| Qui demande | Rôle | Temps accordé |
|---|---|---|
| Un visiteur de votre app non connecté | anon | 3 secondes |
| Un utilisateur connecté de votre app | authenticated | 8 secondes |
| Votre serveur, avec la clé secrète | service_role | aucune en propre, donc les 8 secondes du rôle authenticator s'appliquent |
| Vous, dans le dashboard et le SQL Editor | postgres | aucune en propre, plafonnée par une valeur par défaut de 2 minutes |
Le rôle de l'éditeur a un second avantage. Le guide Row Level Security de
Supabase indique que sur Supabase postgres a bypassrls, l'attribut Postgres
qui ignore toutes les règles. L'éditeur répond donc sans lire une seule règle.
La demande de votre app lit chaque règle de la table sur chaque ligne qu'elle
regarde, avec un minuteur qui sonne au bout de trois secondes pour un visiteur
non connecté. L'éditeur a chronométré une autre requête que celle que votre app
envoie.
Dans la bibliothèque, vous faites partie du personnel. Vous entrez dans la réserve sans minuteur, et personne ne vous demande votre carte. Le visiteur a trois secondes, et la bibliothécaire vérifie les règles sur chaque livre en chemin.
Trouvez la requête qui a été annulée
Cherchez les mots statement timeout dans les logs Postgres de votre projet,
et notez quel rôle a envoyé la requête.
Le guide de Supabase sur les timeouts donne une requête pour cela. Dans le dashboard, ouvrez Explorer, choisissez Run SQL, mettez Logs comme source, choisissez une plage de temps qui couvre l'échec, et lancez cette version raccourcie :
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;
Un user_name égal à authenticator signifie que la demande est passée par la
Data API, le serveur par lequel passe chaque appel supabase.from(…) de votre
app. Le tableau des rôles du guide donne authenticator pour PostgREST, qui
est ce serveur. La colonne query est le SQL qu'est devenue la demande de
votre app, et le nom de table qui y figure est la table à regarder.
La page Query Performance du dashboard est l'autre endroit où l'on regarde, et le guide prévient qu'elle ne liste que les requêtes lentes qui ont abouti. Une requête annulée se trouve dans les logs.
Si les logs montrent des annulations sur toutes les tables à la fois, la partie lente n'est pas une seule règle, et distinguer une panne de Supabase d'un problème dans votre app est le point de départ.
Chronométrez-la comme votre app l'exécute
Lancez la requête avec le rôle de votre app et explain analyze devant, et
Postgres affiche le temps qu'elle a pris et le nombre de lignes que la règle a
refusées.
C'est la méthode du guide de Supabase sur les performances de Row Level
Security, entre begin et rollback pour que le changement de rôle s'arrête
avec le bloc. Copiez l'identifiant d'un vrai utilisateur depuis
Authentication → Users à la place des zéros :
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;
Deux lignes de la sortie comptent. Execution Time est le nombre à comparer
avec 3 et 8 secondes. Rows Removed by Filter compte les lignes que Postgres a
lues et que la règle a refusées, et le guide de Supabase en fait le signal à
surveiller : une règle qui écarte la plus grande partie de la table à chaque
lecture est une règle dont la colonne de filtre a besoin d'un index.
Pour chronométrer ce que reçoit un visiteur non connecté, écrivez
set local role anon; et retirez la ligne des claims.
Le même guide propose de lancer la requête une fois avec Row Level Security activé et une fois désactivé, pour voir si le coût vient de la règle. Il dit aussi de ne le faire qu'en dehors de la production, parce que le désactiver expose chaque ligne. Sur une app en ligne, comparez plutôt la règle à sa version réécrite.
Comment une règle peut-elle coûter des secondes ?
Postgres vérifie la règle sur chaque ligne que lit la requête, et un appel de fonction dans la règle s'exécute de nouveau pour chacune de ces lignes.
Supabase a publié des mesures sur ce point, prises sur une table de 100 000 lignes. En voici quatre, la manière lente et la manière rapide :
| Ce que vérifie la règle | Écrite comme | Durée | Réécrite comme | Durée |
|---|---|---|---|---|
| Les lignes qui appartiennent à l'utilisateur connecté | auth.uid() = user_id | 179 ms | (select auth.uid()) = user_id | 9 ms |
| La même règle, sans index sur la colonne | auth.uid() = user_id | 171 ms | la même, avec un index sur user_id | moins de 0,1 ms |
| Une vérification d'admin qui lit une autre table | is_admin() | 11 000 ms | (select is_admin()) | 7 ms |
| Un rôle cherché dans une autre table | has_role() = role | 178 000 ms | (select has_role()) = role | 12 ms |
Les deux premières sont lentes et restent pourtant sous les trois secondes sur cette table. Les deux dernières dépassent toute limite qu'a une demande d'app. Une fonction helper qui lit une seconde table, du genre qu'on écrit pour savoir si quelqu'un est admin, est appelée une fois pour chaque ligne de la première, donc sur cette table elle a lu la seconde 100 000 fois.
Dans la bibliothèque, ce helper, c'est la bibliothécaire qui va jusqu'au bureau
de l'entrée demander si vous faites partie du personnel, une fois pour chaque
livre du rayon. L'envelopper dans (select …), c'est demander une fois et
noter la réponse sur le papier.
Le Performance Advisor de Supabase en repère une partie. Sa règle nommée Auth
RLS Initialization Plan signale une règle qui "re-evaluates current_setting()
or auth.is_admin(), sort
de cette description, donc une page d'advisor vide n'a pas vérifié vos
helpers. Lisez vous-même les règles de la table à la recherche de tout appel de
fonction qui n'est pas enveloppé.
Corrigez dans cet ordre
Enveloppez les appels de fonction, indexez la colonne que la règle compare, puis nommez le rôle. Chaque étape est un court bout de SQL que vous pouvez lancer dans l'éditeur ou donner à votre builder.
1. Enveloppez chaque appel de fonction dans (select …).
alter policy "read own orders" on public.orders
using ( (select auth.uid()) = user_id );
alter policy garde le nom et le rôle de la règle et remplace la condition.
Utilisez le nom de votre règle et la colonne de votre table, et faites de même
avec un helper : (select private.is_admin()). Supabase pose une condition :
cela ne marche que pour une valeur qui reste la même d'une ligne à l'autre.
Pourquoi les parenthèses font la différence est expliqué dans
le guide pour activer Row Level Security,
et un helper écrit pour
résoudre une erreur de récursion infinie
est exactement le genre qui en a besoin.
2. Indexez la colonne que la règle compare.
create index orders_user_id_idx on public.orders (user_id);
Le guide de Supabase ajoute un piège. Une colonne ne compte comme indexée que
lorsqu'elle vient en premier dans l'index, donc une table dont la clé primaire
est (team_id, user_id) n'a pas d'index sur user_id. Sur une grande table
dans laquelle on écrit pendant que vous travaillez, la documentation de
Postgres indique qu'un create index ordinaire verrouille la table contre les
écritures jusqu'à la fin de la construction. create index concurrently le
construit sans ce verrou, et il doit être lancé comme une instruction seule,
hors d'une transaction.
3. Nommez le rôle visé par la règle. Une règle écrite to authenticated
ne s'exécute jamais pour un visiteur non connecté. Dans les mesures de
Supabase, une demande non connectée face à une règle sans to a pris 170 ms,
et avec to authenticated moins de 0,1 ms.
4. Filtrez aussi dans la demande. Une règle agit comme un where
invisible sur chaque requête, et le guide de Supabase recommande de le répéter
dans la demande de votre app, .eq('user_id', userId), parce que Postgres peut
alors planifier une requête plus rapide. Dans leurs mesures, cela a fait passer
une requête de 171 ms à 9.
5. Retirez la jointure d'une règle d'équipe. Une règle qui compare chaque ligne à une table de membres fait cette recherche ligne par ligne :
using ( (select auth.uid()) in (
select user_id from team_user where team_user.team_id = projects.team_id
) )
Le guide de Supabase la réécrit pour lire les équipes une seule fois et comparer à cette liste :
using ( team_id in (
select team_id from team_user where user_id = (select auth.uid())
) )
Relancez ensuite le explain analyze de tout à l'heure, et comparez
Execution Time et Rows Removed by Filter avec ce que vous aviez avant.
Faut-il augmenter le statement timeout ?
Seulement pour une requête que vous avez mesurée, réécrite et dont vous avez encore besoin, et alors pour cette fonction-là plutôt que pour un rôle entier.
Les limites sont voulues. Le guide d'import de données de Supabase indique
qu'elles servent à répartir équitablement les ressources et à empêcher les
requêtes longues de peser sur tout le système. La limite de anon est partagée
par tous les visiteurs non connectés, et n'importe qui peut envoyer cette
demande avec la clé publique livrée dans votre app. La passer de trois à
trente secondes donne à chacune de ces demandes trente secondes de votre base.
Quand une tâche prend vraiment plus longtemps, donnez la limite à la fonction
qui l'exécute. Le guide de Supabase décrit cela pour les fonctions récurrentes
qui ont besoin d'une exception sur leur durée, et cela marche pour une fonction
que votre app appelle avec supabase.rpc(…) :
alter function public.monthly_totals() set statement_timeout to '20s';
Si vous modifiez tout de même un rôle, les commandes du guide sont
alter role authenticated set statement_timeout = '15s'; puis
notify pgrst, 'reload config';, pour que la Data API prenne la nouvelle
valeur. Supabase plafonne à 60 secondes les requêtes de ses bibliothèques
clientes, quoi que vous régliez.
Deux corrections qui font taire le message et ouvrent la table
Désactiver Row Level Security sur la table, et envoyer la demande avec la clé secrète. Les deux rendent la requête rapide parce que les deux sautent la règle, et les deux livrent les lignes à des gens qui n'auraient jamais dû les avoir.
Désactiver. Sans règle à exécuter, rien n'est vérifié ligne par ligne et le
timeout disparaît. Supabase imprime sa propre mise en garde à côté du test qui
le suggère : désactiver Row Level Security expose chaque ligne de la table à
tout rôle qui a le droit correspondant. Son guide indique aussi que, sur les
projets existants, une nouvelle table dans public démarre avec select,
insert, update et delete accordés à anon, le rôle d'un visiteur non
connecté. Une fois la règle partie, ces droits sont tout ce qui sépare ce
visiteur de la table.
La clé secrète. Le guide de Supabase indique que le rôle service_role
derrière elle a lui aussi bypassrls, donc aucune règle ne s'exécute. Cette
clé a sa place sur un serveur, et une clé envoyée depuis le navigateur est une
clé que n'importe qui peut lire dans votre site ;
quelles clés sont sûres dans un frontend
passe la paire en revue. Elle ne fait pas gagner beaucoup de temps non plus,
car le guide donne à service_role les mêmes 8 secondes qu'à authenticator.
Savoir si l'une de vos tables répond à une demande sans aucune connexion est l'une des neuf vérifications de l'analyse gratuite de Reeve. Elle lit votre site en ligne comme un visiteur le voit, prend environ 20 secondes et ne demande aucun compte.
Garder la règle rapide et la table fermée
Une règle corrigée aujourd'hui aura une version suivante. Votre builder écrit des règles chaque fois qu'une fonctionnalité en a besoin, donc la suivante peut de nouveau appeler une fonction par ligne, ou ouvrir la table pour qu'un écran se charge.
Reeve Monitor vérifie votre app de l'extérieur, toutes les heures :
- les neuf vérifications toutes les heures, y compris si une table répond à une demande sans connexion
- si l'application répond, toutes les 60 secondes
- un message quand un résultat change, pour qu'une table qui se met à répondre à des inconnus n'attende pas que vous regardiez
- un rapport mensuel de ce qu'il a vu
Reeve Care garde une copie de votre base Supabase d'avant votre modification :
- une copie chiffrée selon un calendrier, conservée sur le stockage de Reeve dans l'UE, en dehors de votre compte Supabase
- chaque copie vérifiée avant de compter, en comptant les lignes de chaque table
- une restauration en un clic qui sauvegarde ce qui est là avant de le remplacer
- vos fichiers téléversés aussi, dès que vous connectez un accès Storage
- tout ce que fait Monitor
Ce qu'il faut faire aujourd'hui
Que faire
- Cherchez
statement timeoutdans les logs Postgres et notez la requête et le rôle qui l'a envoyée. - Chronométrez cette requête avec
explain analyzeenanonou enauthenticated, les rôles qu'utilise votre app, et lisezExecution TimeetRows Removed by Filter. - Enveloppez dans
(select …)chaque appel de fonction des règles de cette table, y compris vos propres helpers que le Performance Advisor ne signale pas. - Indexez la colonne que compare chaque règle, et ajoutez
to authenticatedà chaque règle destinée aux seuls utilisateurs connectés. - Relevez une limite en dernier, sur la seule fonction qui en a besoin, et laissez Row Level Security activé.
Commencez par la table derrière la page qui a échoué. Si elle n'a pas encore de règles à elle, activer Row Level Security sur chaque table donne les trois formes dont la plupart des tables ont besoin.
FAQ
Que veut dire "canceling statement due to statement timeout" sur Supabase ?
Postgres a arrêté votre requête parce qu'elle a duré plus longtemps que ne le permet le rôle qui l'a envoyée, et a annulé ce qu'elle avait fait. Supabase donne 3 secondes à un visiteur non connecté et 8 à un utilisateur connecté. L'erreur porte le code 57014, que Postgres utilise aussi pour une requête annulée sur demande, donc lisez la phrase en plus du numéro.
Pourquoi ma requête marche dans le SQL Editor mais part en timeout dans mon app ?
Parce que le SQL Editor travaille avec le rôle postgres, et votre app non. Supabase ne donne pas de limite propre à postgres, et ce rôle a bypassrls, donc l'éditeur n'exécute jamais vos règles Row Level Security. Votre app tourne en anon ou en authenticated, avec une limite de 3 ou 8 secondes, et chaque règle est vérifiée sur chaque ligne que lit la requête.
Row Level Security peut-il provoquer un statement timeout ?
Oui. Postgres vérifie la règle sur chaque ligne que lit la requête, et un appel de fonction dans la règle s'exécute de nouveau pour chacune. Dans des mesures publiées par Supabase sur une table de 100 000 lignes, une règle qui appelait un helper is_admin() a pris 11 000 millisecondes, et la même règle avec l'appel enveloppé en (select is_admin()) en a pris 7.
Comment augmenter le statement timeout sur Supabase ?
Pour une tâche lente précise, réglez-le sur la fonction qui l'exécute avec alter function … set statement_timeout. Pour un rôle entier, lancez alter role avec le nouveau statement_timeout, puis notify pgrst, 'reload config' pour que la Data API le prenne en compte. Supabase plafonne à 60 secondes les requêtes de ses bibliothèques clientes. Corrigez d'abord la règle, car la limite de anon est partagée par tous les visiteurs non connectés.
Faut-il désactiver RLS pour accélérer la requête ?
Non. La requête va plus vite parce qu'aucune règle ne s'exécute, et la table est alors ouverte à tout rôle qui a un droit dessus. Le guide de Supabase indique que, sur les projets existants, une nouvelle table dans public démarre avec select, insert, update et delete accordés à anon, le rôle d'un visiteur non connecté. Enveloppez plutôt les appels de fonction et ajoutez un index.