SQLite gemessen, bei Postgres geblieben
Die Frage war: Könnten kleine Dienste auf eingebettetes SQLite umziehen und so Ressourcen auf dem Mac Mini sparen? Wir haben es nicht geschätzt, sondern gebaut, gemessen und gegen die Produktion gespiegelt — und uns danach bewusst gegen den Wechsel entschieden.
Worum es geht
Der ganze Verein läuft 24/7 auf einem Mac Mini (16 GB RAM) plus einer GPU-Box. Auf einem Gerät dieser Größe ist jedes Hundert-Megabyte ein echtes Argument. Also die naheliegende Frage: Unsere Datenbank-Architektur gibt jedem Dienst seine eigene Postgres-Datenbank — aber die ganz kleinen, lese-lastigen Dienste (der Link-Resolver, die Statistik-Schnappschüsse) brauchen die volle Maschinerie eigentlich nicht. Könnten die auf eingebettetes SQLite umziehen — eine Datei statt eines Datenbank-Anschlusses — und so dem Mini Luft verschaffen? Und das viel gehypte „Turso“ (libSQL) — bringt das etwas?
Wir hätten das überschlagen können. Stattdessen haben wir es gebaut und gemessen, weil eine Datenbank-Engine zu den Entscheidungen gehört, die man besser nicht aus dem Bauch trifft.
Was wir gebaut haben
Als Pilot diente mana-links, der Cross-App-Link-Resolver — der
sauberste Kandidat: zwei Tabellen, fast nur Lese-Zugriffe, keine
exotischen Postgres-Eigenheiten. Die Datenschicht wurde hinter eine
neutrale Schnittstelle gelegt, sodass derselbe Dienst unverändert gegen
drei Backends laufen kann, nur über eine Umgebungsvariable umschaltbar:
- Postgres (wie bisher),
- eingebettetes SQLite (Bun-intern, null externe Abhängigkeit),
- libSQL/Turso (der Sync-fähige SQLite-Abkömmling).
Dazu zwei Mess-Werkzeuge: ein Mikro-Benchmark, der jede Engine isoliert (jedes Backend im eigenen Prozess, Speicher von außen gemessen), und ein Shadow-Read-Vergleich — zwei echte Instanzen auf identischen Daten, derselbe Anfrage-Verkehr gegen beide gespiegelt, Antworten byte-genau verglichen.
Was die Zahlen sagen
Eingebettetes SQLite ist lokal durchweg schneller als Postgres:
| Operation | Postgres | SQLite | |
|---|---|---|---|
| voller Lese-Scan | 0,835 ms | 0,261 ms | 3,2× |
| Punkt-Lookup | 0,100 ms | 0,037 ms | 2,7× |
| Schreiben (Insert) | 0,116 ms | 0,070 ms | 1,7× |
| paralleler Lese-Durchsatz | 1.636/s | 3.664/s | 2,2× |
| Startzeit bis bereit | 53 ms | 23 ms | 2,3× |
Und der Shadow-Vergleich auf prod-nahen Daten: 3.600 von 3.600 Antworten byte-genau identisch, 100 % Parität. Die Engine-Umstellung verändert das Verhalten kein bisschen — der eigentlich wichtige Beweis.
Aber genau dieser Vergleich brachte die nüchterne Einordnung: Der
Geschwindigkeitsvorteil zeigt sich nur bei ungecachten Datenbank-
Zugriffen. Der heiße Pfad des Resolvers liegt hinter einem
5-Minuten-Cache — und ist damit engine-egal. Im Alltag würde
mana-links den SQLite-Vorsprung kaum spüren.
Die ehrliche Überraschung bei Turso/libSQL
libSQL — die Technik hinter Turso — war lokal langsamer als das schlichte eingebettete SQLite, bei Schreibzugriffen sogar langsamer als Postgres. Sein JS-Client trägt Overhead, weil er für etwas anderes gebaut ist: Replikation und Sync über Standorte hinweg, nicht für rohen lokalen Durchsatz. Klare Erkenntnis: libSQL lohnt sich nur, wenn man die Sync-Fähigkeit wirklich braucht (etwa ein Lese-Standby an einem zweiten Schweizer Standort) — nie wegen lokaler Performance. Wer keinen Sync braucht, fährt mit dem eingebauten SQLite strikt besser.
Was es auf dem Host wirklich spart
Hier wurde die Antwort am ehrlichsten — und am unspektakulärsten. Der Ruhe-Speicher eines Dienstes ändert sich kaum (62 statt 67 MB; beides dominiert von der Laufzeit, nicht von der Datenbank). Die echte Ersparnis sitzt bei Postgres selbst: Jede offene Verbindung ist dort ein eigener Prozess. Vier kleine Dienste umzustellen würde rund 20 Verbindungen einsparen — grob 100 bis 200 MB auf dem Mini.
Spürbar, aber kein Befreiungsschlag. Und entscheidend: der zentrale Postgres-Container bliebe — er bedient rund fünfzig Datenbanken; vier herauszulösen entfernt ihn nicht. Wer nur RAM frei machen will, hat einen billigeren Hebel: die Verbindungs-Pools der kleinen Dienste schlicht kleiner stellen — ohne den Stack zu spalten.
Die Entscheidung: bei Postgres bleiben
Wir bleiben bei Postgres als Norm. Die Gründe, in der Reihenfolge ihres Gewichts:
- Performance ist nicht das Argument. Der Cache deckt den heißen Pfad ab; der Messvorteil träfe den Alltag kaum.
- Der einzige echte Gewinn wäre Betrieb (eine Abhängigkeit weniger,
schnellerer Start, eine Datei statt eines
pg_dumpfürs Backup) — und Betriebsqualität lässt sich nicht benchmarken, nur erleben. - Der Preis wäre ein gespaltener Stack. „Postgres für groß, SQLite für klein“ heißt zwei Backup-Wege, zwei Denkmodelle, und für die SQLite-Dienste der Verlust unserer zentralen Postgres-Werkzeuge — Introspektion, Monitoring-Dashboards, das eine nächtliche Backup über alle Datenbanken. Das widerspricht unserem Grundsatz, den Stack einheitlich und ersetzbar zu halten.
Kurz: Wir wechseln nicht aus Neugier auf das Neue. Bewährte, langweilige Technik schlägt das Spannende, solange sie trägt — und Postgres trägt. Das Messen war kein verschwendeter Aufwand: Es ist der Unterschied zwischen „wir glauben, Postgres ist richtig“ und „wir wissen es, mit Zahlen, und können sagen, wann sich das ändern würde“.
Was offen bleibt
- Der Pilot-Code ist fertig und liegt bereit. Sollte der Mini je unter echten Speicherdruck geraten — oder sollte uns das Backup-Argument (eine Datei lässt sich trivial an einen zweiten Standort spiegeln, was unsere größte offene Infrastruktur-Lücke ist) wichtiger werden —, ist der Weg einen begründeten Praxis-Test wert, an genau einem Dienst.
- libSQL bleibt verdrahtet, aber bewusst aus: als möglicher Pfad zu einem Lese-Standby am geplanten zweiten Schweizer Standort, nicht als Performance-Spielerei.
- Schreib-lastige Dienste und Job-Warteschlangen waren ohnehin nie Kandidaten — SQLite kann nur einen Schreiber gleichzeitig.
Wer mitdenken will: [email protected].
mana