Sicherheitsgrundlagen
Supabase "canceling statement due to statement timeout" erklärt
Supabase "canceling statement due to statement timeout": Eine Abfrage lief länger als ihre Rolle darf, 3 Sekunden für Besucher. Prüf zuerst die RLS-Regel.

Kurz gesagt
- "canceling statement due to statement timeout" in Supabase heißt, Postgres hat eine Abfrage gestoppt, weil sie länger lief, als die Rolle darf, die sie geschickt hat: 3 Sekunden für Besucher ohne Anmeldung, 8 für angemeldete. Von dem, was die Abfrage tat, bleibt nichts.
- Im SQL Editor läuft dieselbe Abfrage durch, weil der Editor als Rolle postgres arbeitet. Die hat kein eigenes Limit und überspringt deine Row-Level-Security-Regeln ganz. Deine App prüft jede Regel an jeder Zeile, die sie liest.
- Reparier die Regel, bevor du das Limit anfasst: Funktionsaufrufe in (select …) packen, die verglichene Spalte indexieren, die Rolle nennen. Ein höheres Limit für Besucher gibt jedem Fremden mehr Zeit mit deiner Datenbank.
Die Seite in deiner App lädt ihre Liste, der Spinner dreht sich ein paar Sekunden, und dann kommt nichts. In der Browser-Konsole, oder in einer roten Meldung, die dein Builder eingebaut hat, steht eine Zeile:
canceling statement due to statement timeout
Du öffnest dieselbe Tabelle in Supabase, und jede Zeile ist da. Du fragst den Builder, mit dem du die App gebaut hast (Lovable, Bolt, v0, Cursor, Claude Code oder Replit), welche Abfrage diese Seite schickt, fügst sie in den SQL Editor ein, und sie ist im Nu zurück.
Der erste Rat, den du zu "canceling statement due to statement timeout" in Supabase findest, lautet: das Limit erhöhen. Dieses Limit tut seine Arbeit, und jeder Besucher deiner App teilt es. Bevor du irgendetwas erhöhst, sieh dir die Row-Level-Security-Regel auf dieser Tabelle an, also die Regel, die Postgres prüft, bevor es eine Zeile herausgibt. In Supabases eigenen Messungen ist das die Stelle, an der eine Abfrage, die 7 Millisekunden dauern sollte, 11 Sekunden verbringt.
Was bedeutet "canceling statement due to statement timeout" in Supabase?
Postgres, die Datenbank in deinem Supabase-Projekt, hat die Abfrage gestoppt, weil sie länger lief, als die Rolle darf, die sie geschickt hat, und hat rückgängig gemacht, was die Abfrage bis dahin getan hatte.
Der Satz kommt von Postgres selbst, mit dem Fehlercode 57014, den Postgres
query_canceled nennt. Supabase reicht ihn unverändert an deine App weiter,
also trägt der Fehler, den dein Code bekommt, code: '57014' und diesen Satz
als Nachricht. Postgres verwendet denselben Code für canceling statement due to user request, einen Abbruch, um den jemand gebeten hat, deshalb sagt dir der
Satz mehr als die Zahl.
Stell dir deine Datenbank als Bibliothek vor, mit einer Ausleihtheke und einem Küchenwecker. Jedes Mal, wenn jemand nach Büchern fragt, stellt die Bibliothekarin den Wecker, bevor sie zu den Regalen geht, und wie lange er läuft, hängt davon ab, wer fragt. Wenn er klingelt, kommt jedes Buch zurück ins Regal, und der Besucher bekommt einen Zettel mit diesem Satz darauf. Sollte die Anfrage etwas ändern, wird auch die Änderung zurückgestellt.
Warum läuft die Abfrage im SQL Editor und scheitert in meiner App?
Weil der SQL Editor als ein anderer Besucher fragt, einer ohne eigenen Wecker und ohne Regeln, die geprüft werden müssen.
Jede Anfrage erreicht Postgres unter einer Rolle, einem Namen, der festlegt, was die Anfrage darf. Supabases Anleitung zu Timeouts nennt das Limit, mit dem jede Rolle startet:
| Wer fragt | Rolle | Erlaubte Zeit |
|---|---|---|
| Ein Besucher deiner App ohne Anmeldung | anon | 3 Sekunden |
| Ein angemeldeter Nutzer deiner App | authenticated | 8 Sekunden |
| Dein Server, mit dem Secret Key | service_role | kein eigenes Limit, also gelten die 8 Sekunden der Rolle authenticator |
| Du, im Dashboard und im SQL Editor | postgres | kein eigenes Limit, begrenzt durch einen Standard von 2 Minuten |
Die Rolle des Editors hat noch einen zweiten Vorteil. Supabases Anleitung zu
Row Level Security sagt, dass postgres auf Supabase bypassrls hat, das
Postgres-Attribut, das jede Regel überspringt. Der Editor antwortet also, ohne
eine einzige Regel zu lesen. Die Anfrage deiner App liest jede Regel auf der
Tabelle an jeder Zeile, die sie ansieht, mit einem Wecker, der für einen
Besucher ohne Anmeldung nach drei Sekunden klingelt. Der Editor hat eine andere
Abfrage gemessen als die, die deine App schickt.
In der Bibliothek gehörst du zum Personal. Du gehst ohne Wecker ins Magazin, und niemand will deinen Ausweis sehen. Der Besucher hat drei Sekunden, und die Bibliothekarin prüft die Regeln an jedem Buch auf dem Weg.
Finde die abgebrochene Abfrage
Durchsuche die Postgres-Logs deines Projekts nach den Wörtern statement timeout und notier dir, welche Rolle die Abfrage geschickt hat.
Supabases Anleitung zu Timeouts hat dafür eine Abfrage. Öffne im Dashboard Explorer, wähl Run SQL, setz die Quelle auf Logs, wähl einen Zeitraum, der den Fehler abdeckt, und führ diese gekürzte Fassung aus:
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;
Ein user_name von authenticator heißt, dass die Anfrage über die Data API
kam, den Server, über den jeder supabase.from(…)-Aufruf deiner App läuft. Die
Rollentabelle der Anleitung führt authenticator als PostgREST, und das ist
dieser Server. Die Spalte query ist das SQL, zu dem die Anfrage deiner App
wurde, und der Tabellenname darin ist die Tabelle, die du dir ansehen musst.
Die Seite Query Performance im Dashboard ist der andere Ort, an dem man nachsieht, und die Anleitung warnt, dass sie nur langsame Abfragen zeigt, die fertig wurden. Eine abgebrochene Abfrage steht in den Logs.
Zeigen die Logs Abbrüche auf allen Tabellen gleichzeitig, liegt der langsame Teil nicht an einer einzelnen Regel, und einen Supabase-Ausfall von einem Problem in deiner App unterscheiden ist der Ort, an dem du anfängst.
Miss sie so, wie deine App sie ausführt
Führ die Abfrage als Rolle deiner App aus, mit explain analyze davor, und
Postgres gibt aus, wie lange sie gebraucht hat und wie viele Zeilen die Regel
abgewiesen hat.
Das ist die Methode aus Supabases Anleitung zur Leistung von Row Level
Security, eingepackt in begin und rollback, damit der Rollenwechsel mit dem
Block endet. Kopier eine echte Nutzer-ID aus Authentication → Users an die
Stelle der Nullen:
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;
Zwei Zeilen der Ausgabe zählen. Execution Time ist die Zahl, die du gegen 3
und 8 Sekunden hältst. Rows Removed by Filter zählt die Zeilen, die Postgres
gelesen und die Regel abgewiesen hat, und Supabases Anleitung nennt sie das
Signal, auf das man achten soll: Eine Regel, die bei jedem Lesen den größten
Teil der Tabelle aussortiert, ist eine Regel, deren Filterspalte einen Index
braucht.
Um zu messen, was ein Besucher ohne Anmeldung bekommt, schreib set local role anon; und lass die Zeile mit den Claims weg.
Dieselbe Anleitung schlägt vor, die Abfrage einmal mit eingeschalteter und einmal mit ausgeschalteter Row Level Security laufen zu lassen, um zu sehen, ob die Regel die Kosten verursacht. Sie sagt auch, das nur außerhalb der Produktion zu tun, weil das Ausschalten jede Zeile offenlegt. In einer Live-App miss stattdessen die Regel gegen ihre umgeschriebene Fassung.
Wie kann eine Regel Sekunden kosten?
Postgres prüft die Regel an jeder Zeile, die die Abfrage liest, und ein Funktionsaufruf in der Regel läuft für jede dieser Zeilen erneut.
Supabase hat dazu Messungen veröffentlicht, an einer Tabelle mit 100.000 Zeilen. Vier davon, der langsame und der schnelle Weg:
| Was die Regel prüft | Geschrieben als | Dauer | Umgeschrieben als | Dauer |
|---|---|---|---|---|
| Zeilen, die dem angemeldeten Nutzer gehören | auth.uid() = user_id | 179 ms | (select auth.uid()) = user_id | 9 ms |
| Dieselbe Regel, ohne Index auf der Spalte | auth.uid() = user_id | 171 ms | dieselbe, mit einem Index auf user_id | unter 0,1 ms |
| Eine Admin-Prüfung, die eine andere Tabelle liest | is_admin() | 11.000 ms | (select is_admin()) | 7 ms |
| Eine Rolle, nachgeschlagen in einer anderen Tabelle | has_role() = role | 178.000 ms | (select has_role()) = role | 12 ms |
Die ersten beiden sind langsam und auf dieser Tabelle trotzdem unter drei Sekunden. Die letzten beiden liegen jenseits jedes Limits, das eine Anfrage einer App hat. Eine Hilfsfunktion, die eine zweite Tabelle liest, die Art, die man schreibt, um zu prüfen, ob jemand Admin ist, wird für jede Zeile der ersten einmal aufgerufen, also hat sie auf dieser Tabelle die zweite 100.000 Mal gelesen.
In der Bibliothek ist dieser Helfer die Bibliothekarin, die für jedes Buch im
Regal zum Büro vorne läuft, um zu fragen, ob du zum Personal gehörst. Ihn in
(select …) zu packen heißt, einmal zu fragen und die Antwort auf den Zettel zu
schreiben.
Supabases Performance Advisor fängt einen Teil davon ab. Seine Regel namens
Auth RLS Initialization Plan meldet eine Regel, die "re-evaluates
current_setting() or auth.is_admin(), fällt nicht unter diese Beschreibung, also hat eine leere
Advisor-Seite deine Helfer nicht geprüft. Lies die Regeln auf der Tabelle selbst
nach Funktionsaufrufen durch, die nicht eingepackt sind.
Reparier es in dieser Reihenfolge
Funktionsaufrufe einpacken, die verglichene Spalte indexieren, dann die Rolle nennen. Jeder Schritt ist ein kurzes Stück SQL, das du im Editor ausführen oder deinem Builder geben kannst.
1. Pack jeden Funktionsaufruf in (select …).
alter policy "read own orders" on public.orders
using ( (select auth.uid()) = user_id );
alter policy behält den Namen und die Rolle der Regel und ersetzt die
Bedingung. Nimm den Namen deiner Regel und die Spalte deiner Tabelle, und mach
dasselbe mit einem Helfer: (select private.is_admin()). Supabase nennt eine
Bedingung: Das funktioniert nur für einen Wert, der von Zeile zu Zeile gleich
bleibt. Warum die Klammern den Unterschied machen, steht in
der Anleitung zum Einschalten von Row Level Security,
und ein Helfer, der geschrieben wurde, um
einen Fehler wegen unendlicher Rekursion zu beheben,
ist genau die Art, die sie braucht.
2. Indexier die Spalte, die die Regel vergleicht.
create index orders_user_id_idx on public.orders (user_id);
Supabases Anleitung nennt einen Haken. Eine Spalte gilt nur dann als
indexiert, wenn sie im Index an erster Stelle steht, also hat eine Tabelle mit
dem Primärschlüssel (team_id, user_id) keinen Index auf user_id. Auf einer
großen Tabelle, in die geschrieben wird, während du arbeitest, sagt die
Postgres-Dokumentation, dass ein normales create index die Tabelle für
Schreibzugriffe sperrt, bis der Index fertig ist. create index concurrently
baut ihn ohne diese Sperre, und es muss als eigene Anweisung laufen, außerhalb
einer Transaktion.
3. Nenn die Rolle, für die die Regel gilt. Eine Regel mit to authenticated läuft für einen Besucher ohne Anmeldung nie. In Supabases
Messungen brauchte eine Anfrage ohne Anmeldung gegen eine Regel ohne to 170
ms und mit to authenticated unter 0,1 ms.
4. Filtere auch in der Anfrage. Eine Regel wirkt wie ein unsichtbares
where an jeder Abfrage, und Supabases Anleitung empfiehlt, es in der Anfrage
deiner App zu wiederholen, .eq('user_id', userId), weil Postgres damit eine
schnellere Abfrage planen kann. In ihren Messungen brachte das eine Abfrage von
171 ms auf 9.
5. Nimm den Join aus einer Team-Regel. Eine Regel, die jede Zeile gegen eine Mitgliedertabelle abgleicht, macht diese Suche pro Zeile:
using ( (select auth.uid()) in (
select user_id from team_user where team_user.team_id = projects.team_id
) )
Supabases Anleitung schreibt sie so um, dass sie die Teams einmal liest und gegen diese Liste vergleicht:
using ( team_id in (
select team_id from team_user where user_id = (select auth.uid())
) )
Dann führ das explain analyze von oben noch einmal aus und vergleich
Execution Time und Rows Removed by Filter mit dem, was du vorher hattest.
Soll ich den Statement Timeout erhöhen?
Nur für eine Abfrage, die du gemessen, umgeschrieben und immer noch gebraucht hast, und dann für diese eine Funktion statt für eine ganze Rolle.
Die Limits sind Absicht. Supabases Anleitung zum Datenimport sagt, die Limits
sollen die Ressourcen fair verteilen und verhindern, dass lange laufende
Abfragen das ganze System beeinträchtigen. Das anon-Limit teilen sich alle
Besucher ohne Anmeldung, und jeder kann diese Anfrage mit dem Publishable Key
schicken, der in deiner App mitkommt. Es von drei auf dreißig Sekunden zu
erhöhen, gibt jeder dieser Anfragen dreißig Sekunden deiner Datenbank.
Wenn ein Job wirklich länger braucht, gib das Limit der Funktion, die ihn
ausführt. Supabases Anleitung beschreibt das für wiederkehrende Funktionen, die
eine Ausnahme bei der Laufzeit brauchen, und es funktioniert für eine Funktion,
die deine App mit supabase.rpc(…) aufruft:
alter function public.monthly_totals() set statement_timeout to '20s';
Wenn du doch eine Rolle änderst, lauten die Befehle der Anleitung
alter role authenticated set statement_timeout = '15s'; und danach
notify pgrst, 'reload config';, damit die Data API den neuen Wert übernimmt.
Supabase begrenzt Abfragen aus seinen Client-Bibliotheken auf 60 Sekunden, egal
was du einstellst.
Zwei Lösungen, die die Meldung stoppen und die Tabelle öffnen
Row Level Security auf der Tabelle ausschalten, und die Anfrage mit dem Secret Key schicken. Beide machen die Abfrage schnell, weil beide die Regel überspringen, und beide geben die Zeilen an Leute, die sie nie haben sollten.
Ausschalten. Ohne Regel wird pro Zeile nichts geprüft, und der Timeout
verschwindet. Supabase druckt seine eigene Warnung direkt neben den Test, der
das vorschlägt: Wer Row Level Security ausschaltet, legt jede Zeile der Tabelle
für jede Rolle mit passender Berechtigung offen. Seine Anleitung sagt auch, dass
bei bestehenden Projekten eine neue Tabelle in public mit select, insert,
update und delete für anon startet, die Rolle eines Besuchers ohne
Anmeldung. Ist die Regel weg, stehen nur noch diese Berechtigungen zwischen
diesem Besucher und der Tabelle.
Der Secret Key. Supabases Anleitung sagt, dass auch die Rolle
service_role dahinter bypassrls hat, also läuft keine Regel. Dieser
Schlüssel gehört auf einen Server, und einer, der aus dem Browser geschickt
wird, ist ein Schlüssel, den jeder aus deiner Seite auslesen kann;
welche Schlüssel im Frontend sicher sind
geht das Paar durch. Viel Zeit bringt er auch nicht, denn die Anleitung gibt
service_role dieselben 8 Sekunden wie authenticator.
Ob eine deiner Tabellen auf eine Anfrage ganz ohne Anmeldung antwortet, ist einer der neun Checks in Reeves kostenlosem Scan. Er liest deine Live-Seite so, wie ein Besucher sie sieht, dauert etwa 20 Sekunden und braucht kein Konto.
Damit die Regel schnell und die Tabelle zu bleibt
Eine Regel, die du heute repariert hast, bekommt eine nächste Fassung. Dein Builder schreibt Regeln, wann immer ein Feature eine braucht, also kann die nächste wieder pro Zeile eine Funktion aufrufen oder die Tabelle öffnen, damit ein Bildschirm lädt.
Reeve Monitor prüft deine App von außen, jede Stunde:
- alle neun Checks stündlich, auch ob irgendeine Tabelle auf eine Anfrage ohne Anmeldung antwortet
- ob die App läuft, alle 60 Sekunden
- eine Nachricht, wenn sich ein Ergebnis ändert, damit eine Tabelle, die plötzlich Fremden antwortet, nicht wartet, bis du nachsiehst
- ein Monatsbericht über das, was er gesehen hat
Reeve Care hält eine Kopie deiner Supabase-Datenbank von vor deiner Änderung:
- eine verschlüsselte Kopie nach Zeitplan, auf Reeves eigenem Speicher in der EU, außerhalb deines Supabase-Kontos
- jede Kopie wird geprüft, bevor sie zählt, durch Zählen der Zeilen in jeder Tabelle
- eine Wiederherstellung per Klick, die sichert, was jetzt da ist, bevor sie es ersetzt
- deine hochgeladenen Dateien ebenfalls, sobald du eine Storage-Zugangsberechtigung verbindest
- alles, was Monitor tut
Was heute zu tun ist
Was zu tun ist
- Durchsuch die Postgres-Logs nach
statement timeoutund notier dir die Abfrage und die Rolle, die sie geschickt hat. - Miss diese Abfrage mit
explain analyzealsanonoderauthenticated, den Rollen, die deine App benutzt, und liesExecution TimeundRows Removed by Filter. - Pack jeden Funktionsaufruf in den Regeln dieser Tabelle in
(select …), auch eigene Helfer, die der Performance Advisor nicht meldet. - Indexier die Spalte, die jede Regel vergleicht, und schreib
to authenticatedan jede Regel, die nur für angemeldete Nutzer gedacht ist. - Erhöh ein Limit zuletzt, an der einen Funktion, die es braucht, und lass Row Level Security an.
Fang mit der Tabelle hinter der Seite an, die gescheitert ist. Hat sie noch keine eigenen Regeln, Row Level Security auf jeder Tabelle einschalten hat die drei Formen, die die meisten Tabellen brauchen.
FAQ
Was bedeutet "canceling statement due to statement timeout" in Supabase?
Postgres hat deine Abfrage gestoppt, weil sie länger lief, als die Rolle darf, die sie geschickt hat, und hat rückgängig gemacht, was sie bis dahin getan hatte. Supabase gibt Besuchern ohne Anmeldung 3 Sekunden und angemeldeten Nutzern 8. Der Fehler trägt den Code 57014, den Postgres auch für eine auf Wunsch abgebrochene Abfrage verwendet, also lies den Satz und nicht nur die Zahl.
Warum läuft meine Abfrage im SQL Editor, aber in meiner App gibt es einen Timeout?
Weil der SQL Editor als Rolle postgres arbeitet und deine App nicht. Supabase gibt postgres kein eigenes Limit, und die Rolle hat bypassrls, also führt der Editor deine Row-Level-Security-Regeln nie aus. Deine App läuft als anon oder authenticated, mit 3 oder 8 Sekunden Limit, und jede Regel wird an jeder Zeile geprüft, die die Abfrage liest.
Kann Row Level Security einen Statement Timeout auslösen?
Ja. Postgres prüft die Regel an jeder Zeile, die die Abfrage liest, und ein Funktionsaufruf in der Regel läuft für jede davon erneut. In Messungen, die Supabase für eine Tabelle mit 100.000 Zeilen veröffentlicht hat, brauchte eine Regel mit einem Helfer is_admin() 11.000 Millisekunden und dieselbe Regel mit dem Aufruf als (select is_admin()) 7.
Wie erhöhe ich den Statement Timeout in Supabase?
Für einen einzelnen langsamen Job setzt du ihn an der Funktion, die ihn ausführt, mit alter function … set statement_timeout. Für eine ganze Rolle führst du alter role mit dem neuen statement_timeout aus und danach notify pgrst, 'reload config', damit die Data API den Wert übernimmt. Supabase begrenzt Abfragen aus seinen Client-Bibliotheken auf 60 Sekunden. Reparier zuerst die Regel, denn das anon-Limit teilen sich alle Besucher ohne Anmeldung.
Soll ich RLS ausschalten, damit die Abfrage schneller wird?
Nein. Die Abfrage wird schneller, weil keine Regel mehr läuft, und die Tabelle steht dann jeder Rolle offen, die eine Berechtigung darauf hat. Laut Supabases Anleitung startet eine neue Tabelle in public bei bestehenden Projekten mit select, insert, update und delete für anon, die Rolle eines Besuchers ohne Anmeldung. Pack stattdessen die Funktionsaufrufe ein und leg einen Index an.