A 16-Year-Old SQLite Bug Was Eating Tailscale's Databases
The WAL-reset race hid in every SQLite release since 2010 — and your bundled copy may still carry it.
For sixteen years, every copy of SQLite shipped with a race condition that could silently destroy your database. It sat in the write-ahead-log code from version 3.7.0 in 2010 — the release that introduced WAL mode — until January 2026's 3.51.2, surviving what is probably the most aggressive test regime of any open-source project on earth. It took Tailscale suffering 19 corruption incidents across six months, plus a paid support contract and a purpose-built debugging tool from the SQLite team, to finally flush it out.
The fix landed in SQLite 3.51.3 this March and in 3.53.0 (3.52.0 was withdrawn days after release over an unrelated defect). If you bundle, vendor, or pin SQLite anywhere — and statistically, you do — this is your cue to check the version string.
An impossible bug
Tailscale's control plane runs on sharded SQLite databases, with each shard snapshotted to S3 every few minutes. To make those snapshots fast and consistent, they took manual control of checkpointing — the process that copies committed pages from the WAL file back into the main database — and ran it aggressively from its own thread, instead of letting SQLite's default auto-checkpoint do it lazily on the writing connection.
Starting in August 2025, shards began corrupting. Randomly. Sometimes hours apart, sometimes weeks, with no correlation to load, time of day, customer, or feature. Corrupted shards meant real outages: admin consoles down, API access gone, new devices unable to join affected tailnets. The forensic breadcrumb that eventually cracked it came from transaction replay logs showing something that shouldn't be possible: data committed by one transaction was invisible to later ones. Tailscale's metrics even showed SQLite claiming to copy more pages out of the WAL than the WAL contained.
That's not the signature of hardware failure or fsync lies — the usual suspects when SQLite corrupts. That's a logic bug. Tailscale bought SQLite's professional support, and the SQLite developers built a tracing shim around the virtual filesystem layer to instrument checkpoints in production. What it caught was a genuine data race: if a write transaction lands at exactly the wrong moment while a checkpoint is resetting the WAL, the checkpointer convinces itself that pages have already been copied into the database file when they haven't. Those pages are then thrown away with the WAL. The database ends up referencing pages that no longer exist anywhere.
Why sixteen years of testing never caught it
SQLite's testing story is legendary — 100% branch coverage, billions of test cases, deliberate fault injection. So how does a corruption bug survive sixteen years?
Because that kind of testing verifies logic, not interleavings. This race needs the writer and the checkpointer on different connections on different threads, colliding within a tiny window during a WAL reset. In the default configuration, none of that happens: auto-checkpoint runs on the same connection that just committed, so the writer and checkpointer can't race each other. The overwhelming majority of SQLite's billions of deployments — phones, browsers, embedded devices — live entirely inside that safe default. The bug was unreachable from the well-trodden path, so no amount of traffic on that path could find it.
Tailscale stepped off the path in three compounding ways: separate checkpointer thread, aggressive checkpoint frequency, and enough shards running hot that a one-in-millions window came up 19 times. Their own conclusion — running boring technology in a non-standard way is a risk — is right, but it undersells the uncomfortable part. Every configuration they used was documented and supported. "Non-standard" here just means "not what the test matrix and the install base hammer on all day."
Who actually needs to move
Here's the practical exposure test: are you in WAL mode, and does anything checkpoint from a connection other than your writer? If both are true, you were in the blast radius until 3.51.3.
That second condition is more common than it sounds, because it's precisely the shape of the modern server-side SQLite stack. Replication tools like Litestream take over checkpointing from their own process by design — that's how they control the WAL lifecycle to ship it elsewhere. Multi-threaded app servers that run a background checkpoint goroutine to keep WAL size bounded: same shape. The "SQLite in production" renaissance of the last five years has been busy building exactly the topology this bug required, which makes it mildly miraculous Tailscale hit it first at scale rather than a thousand smaller shops hitting it silently.
Checking is one line — SELECT sqlite_version(); from your app, since what matters is the library your process actually loaded, not the system binary. Then remember how deep the bundling goes. Go's popular drivers vendor their own SQLite. Python's stdlib links whatever the interpreter was built against. Electron apps, mobile apps, and desktop software ship frozen copies; Plex users have already traced corruption to its bundled 3.39.4, and projects have started pinning fixed versions in their dependency trees. Anything from 3.7.0 through 3.51.2 carries the bug. Target 3.51.3 or 3.53.0+, and skip 3.52.0, which was pulled.
If you're stuck on an old version — embedded firmware, a vendor's frozen build — the mitigation is architectural: let auto-checkpoint on the writing connection do its job, and give up the separate checkpointer until you can upgrade.
The verdict
It's tempting to read this as a knock on SQLite. It's closer to the opposite. A customer paid for support, got a bespoke debugging tool and a root-caused fix for a sixteen-year-old race, and the whole forensic story was published for everyone — Tailscale even left a tripwire in their patched driver that fired two months later, confirming the race was still occurring in production and now being caught. Four months of clean operation since. Most databases' corruption war stories end in a shrug and a restore from backup; this one ends in a changelog entry.
The durable lesson isn't "don't touch checkpoints." It's that a well-tested system is only well-tested for the configurations its tests and its install base actually exercise, and the industry is currently migrating SQLite into configurations neither of those covered. If you're running SQLite like a server database — replicated, sharded, background-checkpointed — you're an early settler, not a beneficiary of billions of battle-tested deployments. Budget for that. Ideally before your transaction logs start showing you impossible things.
Sources & further reading
- How Tailscale helped find the SQLite WAL-Reset bug — tailscale.com
- Release History Of SQLite — sqlite.org
- Tailscale Traces Database Corruption to 16y/o SQLite WAL-Reset Bug — news.ycombinator.com
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 0
No comments yet
Be the first to weigh in.