Supabase-Egress reduzieren: Wie wir nach zwei DB-Umzügen die Wurzel fanden
Es gibt Probleme, die löst man nicht, sondern verschiebt sie nur. Eine ganze Weile war der Datenabfluss (Egress) unserer Web-App genau so ein Fall — und ich gebe ehrlich zu, dass wir es uns dabei lange selbst schwer gemacht haben. Jede kleine Änderung am Vision-Board schob hunderte Kilobyte durch die Leitung, und das alle paar Sekunden. Das Egress-Kontingent bei Supabase schmolz dahin, und statt die Ursache anzugehen, sind wir zweimal komplett auf eine frische Datenbank umgezogen, nur um wieder Luft zu haben.
Das ist Symptombekämpfung in Reinform. Und diese Umzüge taten richtig weh.
Warum zwei Umzüge nicht die Lösung waren
Ein Datenbank-Umzug klingt erst mal sauber, ist es aber nicht. Der Standard-Abzug nimmt nämlich nur den Datenkatalog mit — die Bilder und sonstigen Dateien im Speicher wandern nicht automatisch mit. Also mussten wir für jeden der beiden Umzüge (einmal am 9. Mai, einmal am 1. Juni) eigene Rettungsskripte schreiben, die den Datei-Speicher sichern, wieder einspielen und am Ende vergleichen, ob auch wirklich alles drüben angekommen ist.
Und das Beste: Nach jedem Umzug war das Grundproblem natürlich immer noch da. Wir hatten nur das Kontingent zurückgesetzt, nicht den Verbrauch gesenkt. Es war, als würde man einen undichten Eimer regelmäßig leeren, statt das Loch zu stopfen.
Dazwischen haben wir an allen möglichen Symptomen gedreht: das Speichern gedrosselt, die Echtzeit-Filter verfeinert, sogar einzelne große Inhalte von Hand aus den Daten herausgeschnitten und als JSON-Datei beiseitegelegt — eine Vision mit elf langen Plänen, allein 75 Kilobyte, haben wir so manuell geparkt, nur damit der Daten-Blob nicht weiterwächst. Er wuchs trotzdem.
Die Wurzel: ein einziges riesiges JSON-Feld
Irgendwann haben wir uns hingesetzt und nachgemessen, statt weiter zu reagieren. Und das Ergebnis war ziemlich eindeutig: Die Echtzeit-Updates, multipliziert mit der Größe des Daten-Blobs, machten rund 65 Prozent des gesamten Datenabflusses aus. Die Wurzel war also nicht irgendein einzelnes Feature, sondern die Architektur selbst.
Technisch lag die Sache so: Die Daten eines Vision-Boards steckten komplett in einem JSONB-Blob pro Board. In der Spitze waren das rund 640 Kilobyte, zuletzt immer noch 261. Und jeder noch so kleine Edit schrieb diesen kompletten Blob zurück. Schlimmer noch: Ab etwa 700 Kilobyte wurde es brandgefährlich, weil das Laden dann an ein Zeitlimit stieß. Wir liefen also sehenden Auges auf einen Kipppunkt zu.
Ein wachsender Single-Row-Blob ist eine tickende Uhr
Er funktioniert tadellos — bis er es plötzlich nicht mehr tut. Solange alles im grünen Bereich liegt, merkt man nichts. Genau das macht ihn so tückisch.
Die bewusste Warte-Entscheidung
Hier kommt etwas, auf das ich im Rückblick ein bisschen stolz bin: Wir haben die Wurzel-Lösung erst einmal bewusst geparkt. Die Überlegung am 9. Juni war, dass sich ein großer Struktur-Umbau ohnehin abzeichnete — und dass man dann beides in einem Schnitt erledigen könnte, statt zweimal am offenen Herzen zu operieren.
Und genau so kam es. Ein neues Feature — frei befüllbare Bereiche an jeder Karte — brauchte sowieso einen Umbau, wie die Daten gespeichert werden. Zwei Fliegen, eine Klappe. Manchmal ist das Geduldigste tatsächlich das Effizienteste.
Der Schnitt: Skelett bleibt, Inhalte ziehen aus
Die Lösung selbst ist im Kern simpel. Der Blob behält nur noch das Skelett: Struktur, Titel, Reihenfolge, Anheftungen, Eltern-Bezüge und ein paar Zähler. Die schweren Inhalte — die langen Plan-Texte und Aufgabenlisten — wandern als kleine Einzel-Zeilen in eine bereits bestehende Tabelle, jeweils über einen generischen Schlüssel adressiert.
Das Schöne daran: Diese Tabelle hatte schon ein erprobtes System für Sperren, Konflikte und Echtzeit. Wir mussten also nichts Neues bauen, sondern haben das Bewährte wiederverwendet. Eine einzige Ausnahme gibt es — die Mission bleibt im Blob, weil sie ein Einzelstück ohne eigene Kennung ist.
Wichtig war mir, dass wir uns bewusst gegen die vollständige Normalisierung entschieden haben. Hätten wir alles sauber in Einzeltabellen zerlegt, wären Dinge wie das Umsortieren, das kaskadierende Löschen oder die Anheftungs-Regeln plötzlich kompliziert geworden. So bleiben die im Skelett trivial. Unterm Strich: rund 90 Prozent des Nutzens bei einem Bruchteil des Risikos.
Das Ergebnis kann sich sehen lassen. Ein Inhalts-Edit schreibt jetzt eine kleine Zeile im Kilobyte-Bereich statt des kompletten 261-Kilobyte-Blobs — das sind über 99 Prozent weniger auf dem Schreibpfad. Das Öffnen eines Boards überträgt 60 statt 261 Kilobyte, also gut ein Viertel. Inhalte laden außerdem nur noch verzögert, wenn man eine Karte tatsächlich öffnet. Der 65-Prozent-Posten beim Egress ist damit strukturell weg.
Wie es ablief — und wo es spannend wurde
Das Ganze lief als KI-orchestriertes Großprojekt über drei Tage, in vier Phasen und zehn Arbeitspaketen. Das Reizvolle daran: Zwei KI-Teams — eins für die Web-App, eins für den Server — mussten eine harte Reihenfolge beim Umschalten einhalten und sich darüber über eine geteilte Notiz abstimmen, samt schriftlichem Kontrakt und gemeinsamem Rauch-Test, der am Ende vier von vier Punkten bestand.
Die eigentliche Migration war zweistufig und so gebaut, dass man sie gefahrlos wiederholen kann. Zuerst das Nachfüllen — 134 Plan-Zeilen und 7 Aufgaben-Zeilen, wobei neuere Daten niemals überschrieben werden. Dann die Verifikation, dass wirklich jedes Element mit Inhalt seine eigene Zeile hat und keine Waisen übrig bleiben. Erst danach das Bereinigen der 16 Boards. Davor stand selbstverständlich ein vollständiges Backup, und ich hatte die SQL-Skripte vorher gegen eine lokale Wegwerf-Datenbank getestet, inklusive eines Negativtests: Das Bereinigen ohne vorheriges Nachfüllen muss hart abbrechen und die Daten unangetastet lassen. Tat es.
Der Race-Bug: erst unter Last sichtbar
Und dann kam der Moment, der jede Story braucht. Beim Live-Test verschwanden frisch angelegte Plan-Einträge sofort wieder — vor meinen Augen. Man tippt etwas ein, es erscheint kurz, und ist im nächsten Augenblick weg.
Die Ursache war ein Wettlauf-Fehler. Mehrere Speichervorgänge derselben Zeile liefen parallel und überholten sich gegenseitig. Sie liefen dann mit einem veralteten Sperr-Token in Konflikt-Schleifen, und nach zwei Fehlversuchen rollte die Oberfläche den Stand einfach zurück — daher das „erscheint kurz und verschwindet”. Das Tückische: Sichtbar wurde das überhaupt nur, weil zufällig parallel ein Backup lief und alles auf 13 bis 15 Sekunden pro Anfrage ausbremste.
Last macht Wettlauf-Fehler sichtbar, die unter normalem Tempo monatelang schlummern können.
Der Fix war dann unspektakulär, aber wirksam: eine Schreib-Warteschlange pro Zeile, sodass die Speichervorgänge sauber hintereinander ablaufen, statt sich zu überholen. Dazu ein neuer Test, der genau diese parallelen Speicherungen simuliert, damit der Fehler nicht heimlich zurückkommt.
Ergebnis und ein Gefühl
Wir waren wirklich knapp vor dem Kontingent-Limit, und wir haben es gerade noch geschafft. Jetzt reicht das Kontingent locker, und ein dritter Datenbank-Umzug ist vom Tisch. Diese Erleichterung nach Monaten Leidensweg ist schwer zu beschreiben.
Was mir aber am meisten hängen geblieben ist: Das war echte Teamarbeit. Ich habe die Richtung vorgegeben, die kritischen Freigaben erteilt — Backup, der destruktive Bereinigungsschritt — und live getestet. Den koordinierten Umbau selbst haben zwei KI-Teams gestemmt. Genau diese Aufteilung, Mensch gibt Richtung und Freigaben, KI baut koordiniert, fühlt sich nach der richtigen Art zu arbeiten an.
Weiterlesen
- Die andere Seite derselben Blob-Geschichte: Der Editor speichert nicht — die Grenze sitzt beim Lesen
- Wie wir Routine-Abläufe fest verdrahten: KI-Workflows per Keyword
- Wenn die KI vorschnell „ja” sagt: Cascading Failures
Entdecke mehr
Der Editor speichert nicht — und die Grenze sitzt beim Lesen, nicht beim Schreiben
Ein wachsender JSONB-Blob legte unseren Editor lahm. Der Verdächtige war harmlos, die echte Ursache kontraintuitiv: Lesen scheiterte, nicht Speichern.
LexikonCaching und CDN verstehen — die Schichten, die Header und das schwere Problem
Die Cache-Schichten von Browser bis Datenbank, wie HTTP-Caching-Header funktionieren, was ein CDN macht, warum Invalidierung das schwere Problem ist.
LexikonModerne Bildoptimierung fürs Web
AVIF und WebP statt JPEG/PNG, responsive Images, korrektes Lazy-Loading und width/height gegen CLS — Bilder sind einer der größten Hebel für LCP.