SQLite’s own speedtest1 benchmark, run as the official C tool for the citation-grade number, then ported statement-for-statement to run two ways on the same box — directly embedded, and through sqld over a Hrana WebSocket — so the cost of the “serverless” layer is measured, not assumed.
--size 100First, the number people actually quote: SQLite’s own benchmark tool compiled with -O3 and RTREE enabled, run at its default scale on this single-core machine. It builds and tears down a sequence of 50,000-row tables across the full default testset list — unindexed and indexed inserts, clustered updates, deletes, joins, LIKE, VACUUM, JSON, floating point, a star-cube and a Fossil-style schema — and reports a 6.494 s total. This is the citation-grade ceiling for raw SQLite on this exact box.
| Step | Time |
|---|---|
| 50,000 INSERTs into a table with no index | 0.035 s |
| 50,000 unordered INSERTs with one index/PK | 0.074 s |
| 50,000 INSERTs with three indexes | 0.155 s |
| 10,000 SELECTs, text-BETWEEN, indexed | 0.143 s |
| 10,000 four-way joins | 0.362 s |
| 50,000 UPDATEs of individual rows | 0.105 s |
| 50,000 DELETEs of individual rows | 0.121 s |
| 70000 REPLACE ops on an IPK | 0.143 s |
| Star query over a 5,000-entry fact table | 0.028 s |
| Open and use the database 300 times | 0.124 s |
| TOTAL (all testsets) | 6.494 s |
A faithful port of speedtest1’s statement shape — batched inserts in growing group sizes, indexed point and range lookups, a LIKE prefix scan, a full-table scan, updates, deletes, and 100 KB blobs — run twice on the same machine with byte-identical SQL. Embedded, the same engine answers in 483 ms. Through sqlitend’s sqld over a loopback Hrana WebSocket it takes 8,385 ms. There is no network here — the entire delta is one round trip per statement plus sqld’s replication-lite machinery. That is the measured cost of turning a library into a server.
15 steps, zero errors. The base layer. 203 ms alone is the 5,000 individual one-row inserts.
identical statements, zero errors. 4,134 ms is those same 5,000 single-row inserts — each one a Hrana round trip.
| Step | embedded | sqld | Ratio |
|---|---|---|---|
| create schema + indexes | 16 ms | 67 ms | 4.2× |
| insert 5,000 rows · 1 row/statement | 203 ms | 4,134 ms | 20.4× |
| insert 5,000 rows · 10 rows/statement | 50 ms | 495 ms | 9.9× |
| insert 5,000 rows · 100 rows/statement | 21 ms | 148 ms | 7.0× |
| insert 5,000 rows · 1,000 rows/statement | 28 ms | 95 ms | 3.4× |
| insert 2,000 rows · one statement | 9 ms | 39 ms | 4.3× |
| update 1,000 rows via PK | 1 ms | 2 ms | 2.0× |
| update 2,000 rows via index | 13 ms | 30 ms | 2.3× |
| delete 500 rows via PK | 0 ms | 5 ms | — |
| full-table scan (count + sum) | 2 ms | 6 ms | 3.0× |
| 2,000 PK point lookups | 11 ms | 1,278 ms | 116× |
| 2,000 indexed range lookups | 62 ms | 1,655 ms | 26.7× |
| LIKE prefix scan | 7 ms | 55 ms | 7.9× |
| insert 100 × 100 KB blobs | 55 ms | 342 ms | 6.2× |
| read back 50 blobs | 5 ms | 34 ms | 6.8× |
| Total | 483 ms | 8,385 ms | 17.4× |
Read it once and the shape of the answer is obvious. The pain is entirely per-statement: single-row inserts pay 20.4× and one-at-a-time point lookups a brutal 116× (0.64 ms per lookup — that is the round trip itself). The moment rows travel together, sqld’s own SQLite does the exact same work — 1,000 rows per statement lands at 3.4×, which includes the insert logic. The takeaway for real workloads: batch your writes, dashboard your point queries, and the serverless tax mostly disappears. And at no point — embedded or serverless — did a statement fail.
A single sqld database serving this whole suite peaked under 35 MB RSS and never pushed the 1-core box past a load of 0.84. The 17.4× is not the server working hard — it is the server being asked one tiny thing at a time. Each statement could be answered in a blink; the blink is the wire. A faster core, a real network, or 64 of them at once would shift the absolute numbers, not the structure of the answer.
The official tool was built from the stale-free sqlite-amalgamation-3530400 with gcc -O3 plus -DSQLITE_ENABLE_RTREE (without it speedtest1 hard-fails at the rtree testset rather than skipping — 3 runs confirmed), and run at its default size 100. The port is a statement-level re-implementation of that shape — it is not bit-identical to the C tool, and the two totals (6.494 s vs 483 ms) are different workloads, so they are presented separately, never averaged. For the serverless leg, one database was created through the sqlitend API, one token minted, and the port run against ws://127.0.0.1 so no network noise beyond loopback entered the numbers. Both legs used the same Bun 1.4.2 runtime.
Single node, single CPU, deliberately small RAM — absolute seconds are not portable; the ratios and the per-statement shape are the general lesson. Real deployments add a genuine network, TLS, and possibly many concurrent clients, which shift the numbers and are worth a follow-up run. The control plane has no login in v1: put it behind a firewall or an authenticated reverse proxy. This measured what it claims — one engine, two execution paths, the exact same SQL in front of each.