← s13k.dev

Real-workload SQLite benchmark on a $5 VPS

2026-05-09 · s13k

I ordered the cheapest Hetzner CX23 ($4.99/mo, shared-resources tier) and ran a real-workload SQLite benchmark on it. Not a microbenchmark — a 6 GB database on a box with 3.7 GB of RAM, so reads actually have to touch disk.

The box

Shared vCPUs with noisy neighbors:

Latest SQLite 3.53.1, statically linked into a C bench.

SQLite config

Production-realistic pragmas — not turned-up-to-eleven:

journal_mode   = WAL
synchronous    = NORMAL      -- safe, not strict-ACID durable
page_size      = 8192
cache_size     = 256 MiB
mmap_size      = 256 MiB
temp_store     = MEMORY
busy_timeout   = 5000

One caveat worth being explicit about: synchronous=NORMAL is not durable in the strict ACID sense. A power loss can roll back the last few committed transactions — whatever was in the WAL but not yet fsync'd to the main DB file. The database file itself will not corrupt — corruption is what synchronous=OFF risks; FULL avoids the loss entirely; NORMAL only loses the tail. For most apps that's a fine trade for the speedup. If you can't afford to lose any committed write — payments, audit logs — use synchronous=FULL.

WAL also matters for the concurrency numbers below: readers don't block the writer and the writer doesn't block readers, which is why concurrent reads scale almost linearly across cores while writes still serialize.

The workload

Disk-bound results (~6 GB DB > RAM)

Phase Throughput p50 p95 p99 p999
BULK INSERT 10M × 500B 61,300/s 8 µs 58 µs 80 µs 143 µs
SELECT random PK 3,609/s 265 µs 476 µs 715 µs 2.3 ms
SELECT indexed range scan 3,477/s 290 µs 599 µs 895 µs 2.9 ms
UPDATE per-row, 1-txn 2,986/s 257 µs 473 µs 706 µs 2.2 ms
MIXED 70R/25U/5I (OLTP) 3,915/s 257 µs 455 µs 710 µs 2.2 ms
CONCURRENT reads (4 threads, 5s) 14,396/s 162 µs 790 µs 2.2 ms 4.4 ms
CONCURRENT writes (1 thread, 5s) 3,305/s 180 µs 690 µs 1.3 ms 6.0 ms

Working set spills past RAM — every random read pays for I/O.

When the DB fits in RAM

1M rows × 200 B ≈ 246 MB — comfortably cached. This is the engine ceiling on a $5 VPS:

Phase Throughput
INSERT 1-txn (prepared bind) 124,123/s
SELECT random PK 155,828/s
SELECT indexed range 58,239/s
UPDATE per-row 1-txn 89,642/s
UPDATE bulk-join (set-update) 57,519/s
MIXED 70R/25U/5I 53,284/s
CONCURRENT reads (4 threads) 103,894/s
CONCURRENT writes (1 thread) 1,055/s

Pure CPU+lock cost, no disk in the hot path.

In-RAM vs disk-bound

Random reads drop 43× the moment the working set outgrows RAM:

Phase In-RAM DB > RAM Δ
INSERT bulk 124k/s 61k/s 0.5×
SELECT random PK 156k/s 3.6k/s 43×
SELECT range 58k/s 3.5k/s 17×
UPDATE per-row 90k/s 3.0k/s 30×
MIXED OLTP 53k/s 3.9k/s 14×
CONCURRENT reads 104k/s 14k/s
CONCURRENT writes 1.0k/s 3.3k/s 0.3×

Reads collapse. Concurrent writes go the other way — see below.

Counterintuitive: writes are faster on disk

Concurrent writes ran 3.3k/s on disk vs 1.0k/s in RAM. That looked wrong, but here's what's going on:

Making writes faster doesn't help when the bottleneck is taking turns, not doing the work.

Bottom line

SQLite on the cheapest shared-CPU Hetzner handles real production load.

Realistic OLTP mix (70R / 25U / 5I): 3.9k ops/s, p99 = 710 µs, p999 = 2.2 ms.

= 14 million ops/hour on a $5 VPS, with sub-3 ms tail latency.

SQLite is enough for most of us.

Discuss on Hacker News · Discuss on 𝕏 · @s13k_