SpacetimeDB: een korte technische review

Ik zal eerlijk zijn en toegeven dat ik dit smakeloos vind. Desondanks denk ik dat er interessante ideeën in dit product zitten, en ik wil graag een korte technische review schrijven terwijl ik zo eerlijk mogelijk blijf.

Benchmarks

Een veelgemaakte fout van nieuwkomers in de databasesector is de overtuiging dat je kunt winnen door "de beste prestaties" te hebben. In de praktijk heb ik dit nooit gezien. De (weinig) bedrijven die een duurzaam database-aanbod hebben opgebouwd, winnen door goed, eerlijk technisch werk te leveren dat voor zichzelf spreekt. Natuurlijk helpen benchmarks daarbij, maar dan moeten die benchmarks zelf ook het resultaat zijn van goed en eerlijk technisch werk.

De benchmarks die SpacetimeDB heeft verstrekt, zijn dat niet. Er zitten vrij veel technische gebreken in wat er precies wordt gemeten. Er is een alternatieve set benchmarks beschikbaar waarin SpacetimeDB juist zeer slecht afsteekt tegen de concurrentie.

De fundamentele fout in hun benchmarks is echter dat ze niet eerlijk zijn. En ik begrijp waar ze vandaan komen: ze zijn niet eerlijk omdat hun database-aanbod iets heel anders is dan dat van de concurrentie, en dat maakt het verleidelijk om benchmarks op deze manier op te stellen. Hun product bevindt zich in een ander segment van de databasemarkt, maar ze kiezen ervoor om hun product te vergelijken met databases die andere afwegingen maken. Het is een aantrekkelijke vergelijking, maar geen eerlijke.

Ik geef een voorbeeld van hoe dit eruitziet, iets waar ik zelf mee te maken heb gehad: een paar jaar geleden werkte ik bij PlanetScale en lanceerden we een MySQL-extensie voor vector similarity search. We hadden zeer specifieke doelen voor de implementatie; het was heel anders dan alles wat er bestond omdat het volledig transactioneel was en de vectorgegevens op schijf werden opgeslagen, beheerd door de buffer pools van MySQL. Dit staat in contrast met eenvoudigere benaderingen zoals pgvector, die HNSW gebruiken en vereisen dat de similarity graph in het geheugen past. Het was een heel ander product met zeer andere afwegingen.

Het was immens verleidelijk om een EC2-instantie met 32 GB RAM te nemen en daar 64 GB aan vectorgegevens in onze database te laden. Vervolgens deed ik hetzelfde met een Postgres-instantie en pgvector. Dezelfde machine, dezelfde dataset, dezelfde queries! Maar PlanetScale voerde tienduizenden queries per seconde uit, terwijl pgvector meer dan 3 seconden nodig had om één query te voltooien, omdat de HNSW-graph constant tussen schijf en geheugen werd gepaged.

Het was indeed zeer verleidelijk om dat in een benchmark te laten zien: "We zijn 10.000 keer sneller dan pgvector!". Maar dat is niet eerlijk. Ja, het is dezelfde machine, dataset en queries, maar het is niet hetzelfde product. We hebben die benchmarks niet gepubliceerd; in plaats daarvan publiceerden we een technische analyse van de implementatie, zonder oneerlijke vergelijkingen, wat zeer goed werd ontvangen.

Je hebt geen "waanzinnige benchmarks" nodig om te winnen. Je hebt alleen solide technisch werk en solide technische documentatie nodig die de afwegingen en beperkingen van je product uitlegt. Een ander voorbeeld is Turbopuffer. Hun benchmarks zijn niet indrukwekkend, zeker niet vergeleken met hun concurrenten. Hun documentatie bevat meer regels over wat de database niet kan dan over wat hij wel kan. Maar iedereen weet dat als jouw use case past bij hun aanbod, zij het beste zoekproduct op de markt hebben. Ze drinken niet de tranen van hun concurrenten; ze nemen gewoon rustig hun klanten over.

Terug naar SpacetimeDB en hun benchmarks. Zij hebben een heel ander aanbod dan hun concurrenten! Het is een all-in-one database + applicatieserver, waarbij je een database-instantie implementeert en de code van je applicatie binnen de database zelf draait. Dat is een zeer interessant idee. Je zou kunnen zeggen dat het lijkt op stored procedures in een relationele database, maar dan met een betere developer experience. Daar kun je absoluut een levensvatbaar product van maken.

Je moet echter toegeven dat dit zeer weinig te maken heeft met de multi-region, hoogbeschikbare gedistribueerde databases waartegen zij zich benchmarken. Als jouw applicatiecode in de database draait en je concurrenten een aparte applicatie hebben die voor elke query individuele netwerkverzoeken moet doen, dan zul je inderdaad voorlopen in benchmarks die QPS (queries per seconde) meten. Maar zijn dat eerlijke benchmarks? Is dat de vergelijking die je wilt tonen aan potentiële klanten die je technische aanbod evalueren?

Naar mijn mening is dit geen goed punt om te benadrukken. Gegevens in het geheugen benaderen is sneller dan gegevens via een netwerk benaderen, en je hebt een benchmark-omgeving gebouwd om dat te bewijzen. Als potentiële gebruiker ben ik daar niet erg onder de indruk van. Vanuit marketingperspectief zou het veel interessanter zijn om te laten zien hoe snel je gegevens in het geheugen kunt benaderen, en vervolgens de afwegingen uit te leggen die zijn gemaakt om die snelheden te bereiken.

Voor zover ik kan zien, is er op hun website geen duidelijke technische analyse die dit uitlegt. Laten we dat hier eens proberen.

Opslag

Er zijn verschillende redenen waarom SpacetimeDB zulke goede schrijfprestaties laat zien in de synthetische benchmarks die ze hebben gepubliceerd. De belangrijkste factor is dat de applicatielogica lokaal naast de database draait, wat extreem efficiënt kan zijn bij het schrijven naar de datastore. Ze verhogen deze efficiëntie verder met andere trucs (zoals het batchen van writes), maar om bij de getoonde prestatiecijfers te komen, moet er ergens een concessie worden gedaan: de datastore bevindt zich in het geheugen, wat zeer ongebruikelijk is voor een traditionele RDBMS.

Het goede nieuws is dat writes naar de in-memory store lineariseerbaar zijn. Er is echter ook slecht nieuws. Het bewijzen van de lineariseerbaarheid van een systeem is meestal een zware taak; ik hoefde hier geen TLA+ te gebruiken om dat te doen. Hier is het triviaal te bewijzen. Omdat het systeem in feite een hashtabel is met een lock ervoor.

Dit lijkt misschien overdreven, maar geloof me, het is een vrij accurate beschrijving van hoe de storage engine is ontworpen. De gecommitteerde status voor de gehele database in een SpacetimeDB-instantie is ingepakt in een enkele Read-Write Mutex. Alle schrijfoperaties gebeuren sequentieel, wat inderdaad een triviaal bewijs is van lineariseerbaarheid. Twee writes kunnen niet tegelijkertijd plaatsvinden, dus ze kunnen niet conflicteren of racen. Maar een read en een write kunnen ook niet tegelijkertijd plaatsvinden!

Wat gebeurt er als er te veel writes zijn? Worden de readers dan geblokkeerd? Het bouwen van een datastore op basis van een enkele globale lock met read/write-semantiek is een geldige technische keuze. Misschien is het discutabel om dat als "een database" te vermarkten. Maar het lijkt me dat als je volledig voor deze aanpak kiest en die lock de concurrency-controle voor je hele database verzorgt, je zeer expliciete, aanpasbare semantiek nodig hebt voor het prioriteren van readers en writers, om te zorgen dat de server responsief blijft ongeacht de workload.

In dit geval is het gedrag een implementatiedetail, dat nergens specifiek is gedefinieerd of uitgelegd. De mutex is een standaard parkinglot::RWMutex uit de parkinglot crate. Deze heeft eventual fairness, wat betekent dat readers uiteindelijk de lock zullen verkrijgen, zelfs tijdens scenario's met een hoge write-throughput. Ze zullen echter willekeurig worden vertraagd, tot maximaal 0,5 ms. De parking_lot crate is een Rust-port van WebKit's originele WTF::Lock.

Dus wat gebeurt er in dit systeem tijdens een write? Eigenlijk gebeurt er van alles. Het is bijna magisch. Terwijl de globale lock wordt vastgehouden, wordt een Wasmtime-runtime gebruikt om "reducers" uit te voeren (willekeurige gebruikerscode, gecompileerd naar WebAssembly). Terwijl de reducer wordt uitgevoerd, kunnen geen andere reducers worden uitgevoerd en naar de database schrijven. Geen andere code kan op dat moment lezen uit de database. Uit hun officiële documentatie blijkt dat reducers "geen HTTP-verzoeken kunnen uitvoeren". Dat is logisch; de kritieke sectie voor alle writes naar deze database is exclusief en geserialiseerd, en voert willekeurige gebruikerscode uit. Je wilt absoluut geen HTTP-verzoeken doen midden in dat proces.

Er is een ontsnappingsroute: je kunt "Procedures" in de server gebruiken. Sinds de release van deze week zijn deze nog in Beta (de documentatie waarschuwt dat de API in de toekomst kan veranderen). Ze maken het wel mogelijk om zwaardere code uit te voeren, inclusief HTTP-verzoeken, wat een goed ding is. Vanuit een procedure kun je een transactie openen, wat opnieuw de globale mutex activeert en geen andere gelijktijdige writes of reads naar de database toestaat. Zorg er dus voor dat je deze zeer snel commit, anders komt het hele systeem stil te liggen.

Voor reads is het verhaal vergelijkbaar. Deze zouden moeten gebeuren via "Views", wat de read-only variant van reducers zijn. Omdat ze een reader lock op de globale mutex verkrijgen, kunnen meerdere views gelijktijdig draaien, maar de database kan niet worden beschreven terwijl views worden uitgevoerd. Net als reducers zijn views willekeurige gebruikerscode gecompileerd naar WebAssembly.

Duurzaamheid (Durability)

Een direct gevolg van dit single-mutex ontwerp is dat je zo min mogelijk werk moet verrichten in het kritieke pad van de transactie. HTTP-verzoeken zijn uitgesloten, maar je kunt ook geen andere "dure" handelingen doen die andere RDBMS-systemen vaak wel doen, zoals het persisteren van de transactie naar schijf.

Deze volledig in-memory database wordt ondersteund door een Write Ahead Log (WAL), maar de WAL wordt niet als onderdeel van de write-transactie naar schijf geschreven. De WAL is asynchroon en wordt periodiek op de achtergrond naar schijf geflusht (standaard elke 50 ms).

Kun je dit volledig consistent maken? De beperkingen van het "single mutex design" maken dit ingewikkeld, omdat de WAL nooit synchroon geschreven kan worden (dat zou alle andere writes en reads in de applicatie volledig blokkeren). Het systeem biedt wel een optie bij het lezen met specifieke semantiek. De withConfirmedReads-vlag zorgt ervoor dat reads alleen gegevens retourneren die zijn gesynchroniseerd met de schijf, door op de server te wachten (slapen) tot de WAL-vermeldingen voor het resultaat van de query naar schijf zijn geflusht. Dit kan een vertraging van maximaal 50 ms opleveren, wat lang is voor een request. Dit is geen erg ergonomisch gedrag, maar de aanname hier is dat dit een database is voor "grotendeels efemere" gegevens en dat je gemiddelde query niet dit soort strikte consistentiegaranties nodig heeft.

Dit geheel doet sterk denken aan MongoDB uit 2011. In veel opzichten wel. De mensen bij Mongo lanceerden een vrij matige database met zeer indrukwekkende benchmarks en werden uiteindelijk door het internet gedwongen (zie: "MongoDB is Web Scale") om een proper storage engine te implementeren. Ze kochten WiredTiger, wat echt een degelijke storage engine is. Vijftien jaar later zijn ze een serieus en levensvatbaar databasebedrijf. Toch zijn er nog steeds veel technische mensen die zich de begindagen van Mongo herinneren en weigeren het in productie te gebruiken of aan te bevelen. Hun informatie is verouderd; modern Mongo is een serieuze database die werkt. Maar de slechte technische reputatie blijft hangen, en zal dat altijd blijven.

Ik denk dat hier een belangrijke les in zit: als ik in 2026 een databaseproduct zou lanceren dat in feite een hashtabel is met een enkele lock ervoor, dan zou ik dat stilzwijgend doen. Want shortcuts nemen bij de lancering van een databaseproduct is bewezen een levensvatbare aanpak (ik zou het zelf niet doen, maar MongoDB deed het met groot succes). Maar zodra het product aanslaat, moet je haasten om de technische en reputatie-schuld af te betalen. Een marketingvideo met laserstralen en een "fles tranen" maakt dit veel ingewikkelder.

Afwegingen

We hebben de technische keuzes gezien die ervoor zorgen dat SpacetimeDB zo goed presteert in specifieke benchmarks (namelijk benchmarks waarin wordt gemeten hoe snel een applicatie naar de database kan schrijven, gegeven dat de applicatie de database is). Deze keuzes worden niet vooraf in de documentatie uitgelegd, en helaas worden de afwegingen die deze keuzes impliceren ook niet expliciet vermeld.

Kort samengevat:

  • Dit is geen gedistribueerd systeem en het heeft een zeer harde limiet op schaalbaarheid en beschikbaarheid.
  • Je kunt een "SpacetimeDB cluster" implementeren, bestaande uit een primaire instantie en meerdere followers met eventually consistent replicatie (nadruk op eventually consistent; zowel de WAL als de replicatie zijn eventually consistent, wat veel ruimte laat voor fouten), maar je hele systeem wordt beperkt door de CPU- en RAM-capaciteit van de machine waarop de hoofdinstantie draait.
  • Je hebt voldoende CPU nodig voor zowel de databasequeries als de volledige applicatielogica, aangezien de applicatie in de database leeft.
  • Je hebt voldoende RAM nodig om alle databasegegevens in het geheugen te passen. SpacetimeDB is helemaal niet disk-backed; het flusht alleen een WAL naar schijf (en periodiek snapshots om herstel van de WAL bij restarts te versnellen). Als je dataset groter wordt dan het beschikbare RAM, zal je database (en je applicatie) falen. De enige optie voor schaalbaarheid is verticaal: een grotere machine kopen.

Deze afwegingen zijn op zichzelf volkomen valide. Maar ze positioneren SpacetimeDB duidelijk als "een krachtigere Redis", en niet als "een performantere relationele database". Het is zeer raadselachtig waarom de auteurs ervoor kozen om zich te benchmarken als dat laatste.

Use Cases

De originele versie van SpacetimeDB werd ontwikkeld als de backend van een MMORPG (een echt spel dat je nu in Steam kunt spelen). Dat lijkt me redelijk. Ik denk dat alle technische keuzes in de database passen bij deze use case. Het is prima om asynchroon een WAL-vermelding naar schijf te flushen die zegt dat xXxPussyHunter420xXx een item heeft geloot. Een vertraging van 50 ms is hier acceptabel. De speler zal gefrustreerd zijn als de instantie precies op dat moment crasht, maar hij komt er wel over.

Natuurlijk worden er momenteel niet veel MMORPG's gebouwd, en de studio's die multiplayer games maken, geven meestal de voorkeur aan hun eigen in-house backends. Het zijn grote studio's die dit vaker hebben gedaan. Ik begrijp dus volledig waarom ze SpacetimeDB v2 herpositioneren naar iets met een breder bereik.

Hun marketingpagina zegt nu dat "LLM's veel verder gaan met SpacetimeDB omdat het alle persistentie, logica, implementatie en real-time synchronisatie in één samenhangende backend afhandelt." Dat is ook een begrijpelijke keuze. Agentic coding is de huidige trend. Een database bouwen die zich richt op LLM's lijkt een goed idee. Maar ik zal eerlijk zijn: ze hebben letterlijk de slechtst mogelijke technische keuzes gemaakt voor deze use case.

Het hele concept van SpacetimeDB is dat de prestaties en beschikbaarheid van zowel je applicatie als je database voor 100% worden bepaald door korte segmenten gebruikerscode die geen operaties met neveneffecten of vertragingen mogen uitvoeren. Dit komt omdat ze zijn gecompileerd naar WebAssembly en worden uitgevoerd door een virtuele machine binnen een kritieke sectie die alle writes en reads naar de storage backend serialiseert. De afwezigheid van neveneffecten of vertragingen kan niet worden afgedwongen door het typesysteem, en is afhankelijk van de specifieke WASM-bytecode die door een JIT-compiler tijdens runtime wordt gegenereerd. Elke fout binnen deze kritieke secties, elke operatie die kan zorgen voor vertraging onder belasting, zal waarschijnlijk pas in productie zichtbaar worden en de prestaties van je hele applicatie verslechteren — zeer waarschijnlijk tot het punt van een beschikbaarheidsprobleem.

Dit is niet de ideale omgeving voor een LLM om in te programmeren.

Dat gezegd hebbende: ik denk dat er een product in zit en lessen te leren vallen. Misschien passen de auteurs deze lessen uiteindelijk toe op SpacetimeDB v3 en lanceren ze een veerkrachtigere en LLM-vriendelijkere database, waarbij applicatiecode geïsoleerd is en kan draaien zolang het nodig is, zonder andere lokale applicatiecode te beïnvloeden, zelfs bij ernstige implementatiefouten. Een systeem waarbij transacties kunnen draaien zonder de prestaties van andere transacties te beïnvloeden, en waarbij ze impliciet worden geknepen als ze te lang duren (bijvoorbeeld als de LLM geen optimaal queryplan heeft geleverd). Misschien zien we een systeem dat veel veerkrachtiger is tegen storingen, maar met minder "indrukwekkende prestaties"; misschien een systeem dat triviaal gedistribueerd is, zodat de AI-agent zelf geen gedistribueerd systeem hoeft te plannen; misschien een lancering met minder onzinnige benchmarks en meer technische details.

Dat zou een product zijn om in de gaten te houden.