Zum Inhalt springen

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.

Vlad Tkachenko12 Min. Lesezeit
Eine Stoppuhr, deren Zeiger über die Markierung hinaus ist, vor einem langen Regal aus Zeilen, von denen nur die ersten abgehakt sind.

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 fragtRolleErlaubte Zeit
Ein Besucher deiner App ohne Anmeldunganon3 Sekunden
Ein angemeldeter Nutzer deiner Appauthenticated8 Sekunden
Dein Server, mit dem Secret Keyservice_rolekein eigenes Limit, also gelten die 8 Sekunden der Rolle authenticator
Du, im Dashboard und im SQL Editorpostgreskein 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.

Eine Abfrage, drei Rollen. Die Rolle des Editors überspringt die Regel und hat keine Linie, die sie überschreiten kann; ein Besucher ohne Anmeldung durchläuft jede Prüfung und wird bei drei Sekunden gestoppt.

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üftGeschrieben alsDauerUmgeschrieben alsDauer
Zeilen, die dem angemeldeten Nutzer gehörenauth.uid() = user_id179 ms(select auth.uid()) = user_id9 ms
Dieselbe Regel, ohne Index auf der Spalteauth.uid() = user_id171 msdieselbe, mit einem Index auf user_idunter 0,1 ms
Eine Admin-Prüfung, die eine andere Tabelle liestis_admin()11.000 ms(select is_admin())7 ms
Eine Rolle, nachgeschlagen in einer anderen Tabellehas_role() = role178.000 ms(select has_role()) = role12 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.() for each row". Ein Helfer von dir, etwa 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.

Ohne Index liest die Regel jede Zeile, um die wenigen zu finden, die dir gehören. Mit Index geht Postgres direkt zu ihnen.

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 timeout und notier dir die Abfrage und die Rolle, die sie geschickt hat.
  • Miss diese Abfrage mit explain analyze als anon oder authenticated, den Rollen, die deine App benutzt, und lies Execution Time und Rows 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 authenticated an 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.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Sichere deine Supabase-Datenbank vor der nächsten Änderung

Reeve sichert sie verschlüsselt nach Zeitplan und stellt sie für dich wieder her, wenn etwas schiefgeht.

Supabase-Backups ansehen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.