How to Recover a Corrupted SQLite Database: A Real-World Walkthrough
If you have run SQLite in production for more than a few months, you have seen this error:
Error: database disk image is malformed (11)
Your first instinct is usually wrong. This post walks through a real corruption-and-recovery drill, verified against an actual database with the pure-Go modernc.org/sqlite driver. You will learn the recovery ladder, and — more importantly — what SQLite's integrity check cannot see.
Rule 1: stop writing, and copy the files
The moment you see malformed, stop the service. Do not keep writing to the database. Every write makes recovery harder.
Then make a copy before touching anything. In WAL mode a database is three files:
mydb.db # the main database
mydb.db-wal # pending writes not yet checkpointed
mydb.db-shm # shared-memory index (can be rebuilt, but copy it anyway)
Copy all three together — copying only mydb.db while a -wal file exists will silently lose every write that hasn't been checkpointed. On Windows, copy to a folder on the same volume first: copying across drives (for example C: → E:) and replacing in place raises WinError 17 (ERROR_NOT_SAME_DEVICE). Copy, then replace on the same volume.
Never operate on the live file. Work on the copy.
Step 1: characterize the damage
Run an integrity check on the copy:
PRAGMA integrity_check;
It returns one row per problem. A healthy database returns a single ok. The output tells you whether the corruption is structural (a broken page or B-tree) or cosmetic.
Step 2: try VACUUM INTO
VACUUM INTO 'clean.db' (SQLite 3.27+) writes a brand-new, defragmented, structurally valid database to a new file. It is the single most useful recovery command because it works even when the original refuses to open cleanly — and it is also a great backup method for a healthy database.
sqlite3 mydb-copy.db "VACUUM INTO 'recovered.db';"
Then verify the result:
PRAGMA integrity_check; -- on recovered.db -> should return: ok
What actually happened: three verified failure modes
To test this properly, I built a 1,000-row database, then deliberately broke three copies in three different ways. Here is the real output from the drill, generated with modernc.org/sqlite on Windows:
=== SQLite corruption + recovery walkthrough ===
driver: modernc.org/sqlite (pure Go), journal_mode=WAL, synchronous=NORMAL
healthy.db: 1,000 rows in table items(id, name, value, note)
--- Case 1: bit-rot (garbled bytes inside row payloads) ---
SELECT count(*): ok (1000 rows readable despite corruption)
PRAGMA integrity_check: 1 row(s) returned
[1] ok
VACUUM INTO: OK -> recovered.db integrity=true rows=1000
--- Case 2: truncation (power loss / partial write) ---
SELECT count(*): FAILED -> database disk image is malformed (11)
PRAGMA integrity_check: 1 row(s) returned
[1] integrity_check failed: database disk image is malformed (11)
VACUUM INTO: FAILED -> database disk image is malformed (11)
fallback: restore from backup, or use sqlite3 CLI '.recover'
--- Case 3: zeroed page (bad disk block) ---
SELECT count(*): FAILED -> database disk image is malformed (11)
PRAGMA integrity_check: 1 row(s) returned
[1] *** in database main ***
Tree 2 page 7: btreeInitPage() returns error code 11
VACUUM INTO: FAILED -> database disk image is malformed (11)
fallback: restore from backup, or use sqlite3 CLI '.recover'
Three takeaways from the drill:
- Structural corruption is loud. Truncation and zeroed pages break the file structure, so reads fail with
malformed (11),integrity_checkreports the specific page, andVACUUM INTOcannot save you. - Content corruption is silent. In Case 1 the flipped bytes landed inside row payloads —
integrity_checkreturnedokandVACUUM INTOproduced a valid 1,000-row copy. That copy is structurally perfect but its content is wrong. SQLite does not checksum payload bytes, sointegrity_checkwill not notice this. The only defense against silent bit-rot is a backup you can compare against. VACUUM INTOis not magic. When the structure itself is gone (Cases 2 and 3), it fails. This is exactly when your backup — or the CLI's.recovercommand — becomes the plan.
Step 3: when VACUUM INTO fails
Recovery ladder, in order:
- Restore from backup. This should be the outcome 99% of the time. If you are here, the rest of this list is damage control.
sqlite3 mydb-copy.db .recover | sqlite3 recovered.db— SQLite 3.29+ recovers as much data as it can from a badly damaged file by scanning raw pages. Some data is dropped; keys and page structure that cannot be parsed are skipped.- Last resort: set
PRAGMA writable_schema=ONand salvage individual tables. This is invasive — do it only to pull out a few irreplaceable rows.
Prevention is the actual fix
- Back up all three files together in WAL mode (
db+-wal+-shm), or useVACUUM INTOon a running database to take a safe, consistent snapshot. - Run
PRAGMA integrity_checkon a schedule. It is cheap for small databases and catches structural rot early — just remember it does not catch payload corruption. - Test your backups. A backup you have never restored is a wish, not a backup.
- Match your durability settings to your data.
synchronous=NORMALin WAL mode is a sensible default; for data you cannot lose, considerFULLand a redundant copy.
The short version
- Stop writing, copy
db+-wal+-shmto the same volume, work on the copy. PRAGMA integrity_checkto characterize;VACUUM INTOto rebuild what is structurally recoverable.- If
VACUUM INTOfails, restore from backup, then try.recover. integrity_checkwill not catch corrupted row contents — that is why backups matter.
Need to size a SQLite database before you build it? Use the estimator below.
Comments (21)
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp
pwvrlnxodnmqskpwnskfghkgsmyskp