SQLite — the most tested database on the planet by startup count: it is inside every browser, every phone, and half of what you self-host. The story that Tailscale published on August 12 is all the more interesting: they trapped database corruption for six months, reached paid support from the SQLite developers themselves — and together found a race in the checkpoint mechanism that had lived in the code for no less than sixteen years.
Below is the chronology: what it looked like, how they looked for it, what they found, and what this implies for a home server where half of the services run on SQLite.
What it looked like: 19 damages in six months
The symptom was blunt and unpleasant: the database occasionally ended up corrupted. Not “running slowly,” not “locks” — exactly data corruption. In six months — 19 incidents.
Usually such things are blamed on anything other than the DBMS itself: a failing disk, a network filesystem, a process killed by SIGKILL, a broken backup on top of a live database. The list of suspects is long, and each is more plausible than a “bug in SQLite” — a library with one of the densest test suites in the industry.
What WAL and checkpoint are in two paragraphs. In the write-ahead logging mode, a transaction does not write pages to the database file immediately. It appends them to a separate WAL file — this is fast and does not block readers.
Sooner or later, the accumulated data must be transferred to the main file. This operation is called a checkpoint. The official documentation describes its normal mode as: “By default, SQLite will automatically checkpoint whenever a COMMIT occurs that causes the WAL file to be 1000 pages or more in size, or when the last database connection on a database file closes”.
When the checkpoint finished and WAL is not being read, the so-called “reset to the beginning” happens: “the writer will rewind the WAL back to the beginning and start putting new transactions at the beginning of the WAL”. That exact moment — reset WAL — turned out to be the crime scene.
How they searched: a shimmer over a virtual file system
Ordinary debugging here does not work: reproducing a race that happens every few weeks under load cannot be done with a debugger. So they used a technique worth remembering.
SQLite talks to disk not directly, but through a VFS layer — an abstraction with a replaceable implementation. The SQLite developers wrote a shimmer tmstmpvfs, which sits between the library and the real filesystem and logs all disk operations with timestamps. Then they waited for another incident and analyzed the log.
This is arguably the main portable lesson of the story. When a bug is not reproducible but repeats, don’t try to catch it with a debugger. Instrument at the narrowest interface (for SQLite this is the VFS) and wait. Cheaper in effort and almost always more effective.
What they found: a checkpoint that trusted unfinished work
Diagram based on the analysis by Tailscale and Write-Ahead Logging
The mechanism, as explained by Tailscale: if a write arrives at a specific moment during a checkpoint, “the checkpointing process gets confused — it thinks some of the pages have been copied from the WAL into the main database file, but they haven’t.”
Next comes the chain that makes the bug destructive: “Those pages never get written to the database file, and that data is permanently lost. The database file becomes corrupt, because other pages which reference those pages — such as an index — are written to the database.”
In other words, not just data is lost. Pages that other, successfully written pages reference are lost. The index points to emptiness — and the file becomes structurally corrupted, not just incomplete.
The age of the bug was estimated by SQLite developers at at least 16 years. Conditions for triggering are rare: manual control of checkpoint combined with an aggressive frequency of their runs. So rare that for testing they had to add code that deliberately triggers the race.
What they fixed and when
The fix is an additional check in the checkpoint function that detects that the WAL has been reset by another thread. Commit 7168988acbec2d8d.
Next — the detail that many missed in the news. See the official SQLite changelog:
- 3.51.3, March 13, 2026 — “Fix the WAL-reset database corruption bug”.
- 3.52.0, March 6, 2026 — release recalled; all features planned in it were moved to 3.53.0.
So the patch was released in March, quietly, in the 3.51 patch release — and the public breakdown appeared only in August. Five months the bug existed fixed, but almost no one knew exactly what was fixed.
Practical takeaway is not “update urgently” but “check which version you have.” The key number is 3.51.3. Everything older contains the bug.
The difficulty is that “you have” is a fuzzy concept. System sqlite3 and SQLite inside an application are, as a rule, different libraries: many Go, Rust, and Python programs carry their own builds. Updating the package in the distribution does not fix the application that ships with an older, compiled version.
How to check:
# system library
sqlite3 --version
# version actually seen by a specific database
sqlite3 /path/to/app.db "SELECT sqlite_version();"
# integrity check (on a copy, not on the live database)
sqlite3 /path/to/copy.db "PRAGMA integrity_check;"
How this affects a home server
Frank answer: probably not much — and here’s why.
The triggering condition, described in the analysis, is manual checkpoint control plus a high frequency of their launches. The vast majority of self-hosted SQLite-based applications do not do anything like that: they rely on automatic checkpoint at a 1000-page threshold. Tailscale, however, managed checkpoints explicitly and launched them often — the configuration was not ordinary.
From this does not follow that you can relax. From this follows that if you have ever seen database corruption on your setup and attributed it to the disk — it’s worth rechecking the SQLite version in the specific application before swapping SSDs.
And separately: copying a live database with a plain cp is still a way to corrupt the file with your own hands, regardless of any race conditions. Normal options are sqlite3 db ".backup out.db" or VACUUM INTO 'out.db'; both can operate on a database that is currently being written to.
**What makes sense to do in fifteen minutes: **
- Make a list of services you have on SQLite. Usually this is more than it seems: media server, RSS reader, password manager, monitoring panel, smart-home history.
- For each — find the SQLite version:
SELECT sqlite_version();via the application’s normal console or on a copy of the file. - Configure backups in the normal way (
.backup/VACUUM INTO), notcpon a schedule. - Once a month run
PRAGMA integrity_check;on a copy. This is the only way to detect corruption before the app hits it itself.
The story is also a reminder of scale: code that is run through one of the densest test suites in open source carried a race for sixteen years, leading to data loss. Not because someone was sloppy, but because the window of triggering is measured in microseconds and requires a specific usage pattern. “Time-tested” and “tested in all modes” are not the same.
Sources
- Tailscale: We tracked down the 16-year-old WAL-reset SQLite bug — chronology, 19 incidents, shimmer
tmstmpvfs, race mechanism - SQLite changelog — “Fix the WAL-reset database corruption bug”, version 3.51.3 from 13.03.2026, recalled 3.52.0
- SQLite: Write-Ahead Logging — automatic checkpoint at 1000 pages, WAL rewind to the beginning
- Commit 7168988acbec2d8d — the fix
[question]Question to those who caught a “corrupted database” on their system and didn’t get to the cause: what did it end up being — disk replacement, reinstalling the service, restoration from a backup? And does anyone even regularly check integrity_check, or do people find out about the corruption at the moment the application stops starting?
[/question]

