SQLite “database disk image is malformed”: Check, Recover and Fix (Tested)

The error database disk image is malformed sounds like the data is gone, but it doesn’t have to be. In the test below, one damaged page cost 18 rows, and .recover got everything else back. In this article I’ll walk you through checking how bad the damage is and recovering what’s left.

TLDR

  • The error means SQLite found a page in the file that doesn’t have the structure it expects. It’s SQLite’s SQLITE_CORRUPT result code (11).
  • Stop writing to the file and make a copy before you try anything.
  • Run PRAGMA integrity_check; to see which tables or indexes are damaged.
  • Rebuild the database with sqlite3 broken.db .recover | sqlite3 fixed.db, then check the new file.
  • Don’t rely on .dump alone. On a damaged file it ends with ROLLBACK, so loading it restores nothing.

What is “database disk image is malformed”?

SQLite returns this message when it reads part of the database file and finds that it isn’t valid. The result code is SQLITE_CORRUPT, which “indicates that the database file has been corrupted” (SQLite result codes). In Python’s sqlite3 module it arrives as sqlite3.DatabaseError: database disk image is malformed. Some guides call it an OperationalError, but DatabaseError is what Python raised in the test below.

SQLite reads the file one page at a time, so the error shows up only when a query touches a damaged page. Queries on undamaged tables keep working, which is why the error can look random.

What causes it?

SQLite’s documentation lists the ways a file gets corrupted in How To Corrupt An SQLite Database File. SQLite is tested against power failures and crashes, so the causes are outside it, for example:

  • another program or thread writing over the file;
  • copying or restoring the file while a transaction is open;
  • deleting the -journal or -wal file that goes with it;
  • disks or file systems that don’t honor sync requests, and failing hardware.

How to fix a malformed SQLite database

I built a test database with 2,000 users and 3,000 posts for this guide. Then I overwrote one 4 KB page of the posts table with junk bytes. The script, corrupt.py, only does that:

# Overwrites one 4096-byte page of a database file with junk, to simulate a damaged disk block.
import sys
path, page = sys.argv[1], int(sys.argv[2])
with open(path, "r+b") as f:
    f.seek(4096 * (page - 1))
    f.write(b"\xab" * 4096)
print(f"overwrote page {page} of {path}")

Page 50 is a leaf page of posts:

$ sqlite3 app.db "SELECT name, pageno, pagetype FROM dbstat WHERE name = 'posts' AND pageno IN (4, 50);"
posts|4|internal
posts|50|leaf
$ python3 corrupt.py app.db 50
overwrote page 50 of app.db

Step 1: Confirm the error and copy the file

The users table still reads fine. Counting posts has to read page 50, and fails:

$ sqlite3 app.db 'SELECT count(*) FROM users;'
2000
$ sqlite3 app.db 'SELECT count(*) FROM posts;'
Error: stepping, database disk image is malformed (11)

From Python:

import sqlite3

con = sqlite3.connect("app.db")
try:
    con.execute("SELECT count(*) FROM posts").fetchone()
except sqlite3.DatabaseError as e:
    print(type(e).__name__ + ":", e)
$ python3 python_error.py
DatabaseError: database disk image is malformed

Before going further, close every app using the database. Then copy the file along with any -wal or -journal file next to it. Every step below writes only to new files.

Step 2: Find the damage with integrity_check

PRAGMA integrity_check “does a low-level formatting and consistency check of the database” (SQLite PRAGMA). It names the damaged b-tree (tree 4 is posts, whose root page is 4) and the page:

$ sqlite3 app.db 'PRAGMA integrity_check;'
Error: stepping, database disk image is malformed (11)
*** in database main ***
Tree 4 page 50: btreeInitPage() returns error code 11

PRAGMA quick_check is the faster version and found the same page here:

$ sqlite3 app.db 'PRAGMA quick_check;'
Error: stepping, database disk image is malformed (11)
*** in database main ***
Tree 4 page 50: btreeInitPage() returns error code 11

Step 3: Recover the data into a new file with .recover

The sqlite3 shell’s .recover command reads every page it can and writes SQL that rebuilds the database. Pipe it into a new file:

$ sqlite3 app.db .recover > recover.sql
$ head -n 3 recover.sql
.dbconfig defensive off
BEGIN;
PRAGMA writable_schema = on;
$ sqlite3 recovered.db < recover.sql
          defensive off

The defensive off line is the shell echoing a setting from the first line of the recovery script. Check the new file:

$ sqlite3 -header -column recovered.db "PRAGMA integrity_check; SELECT (SELECT count(*) FROM users) AS users, (SELECT count(*) FROM posts) AS posts;"
integrity_check
---------------
ok
users  posts
-----  -----
2000   2982

Every user came back. So did 2,982 of the 3,000 posts. Comparing with a copy taken before the damage shows the 18 lost rows were all on page 50:

$ sqlite3 recovered.db "ATTACH 'app-good.db' AS good; SELECT min(id), max(id), count(*) FROM good.posts WHERE id NOT IN (SELECT id FROM posts);"
199|216|18

The recovery documentation warns that this is normal: “Some content might be permanently deleted and unrecoverable.” Rows that .recover finds but can’t place in a table go into a lost_and_found table.

Step 4: Swap in the recovered file

Keep the damaged file, then put the recovered one in its place and check it:

$ mv app.db app-corrupt.db && mv recovered.db app.db && sqlite3 app.db 'PRAGMA quick_check;'
ok

If you have a backup from before the damage, compare it with the recovered file before deciding which one to keep.

Examples

These show what the other tools do on a damaged file.

Why .dump isn’t enough

.dump exports what it can, but when it hits damage it ends the script with ROLLBACK. Here it runs on the damaged copy, app-corrupt.db:

$ sqlite3 app-corrupt.db .dump > dump.sql
$ grep -c 'INSERT INTO posts' dump.sql
2982
$ tail -n 1 dump.sql
ROLLBACK; -- due to errors

Loading that file creates nothing, because the final ROLLBACK undoes every statement:

$ sqlite3 from_dump.db < dump.sql
$ sqlite3 from_dump.db 'SELECT count(*) AS objects FROM sqlite_schema;'
0

You can change the last line to COMMIT; and load it, but .recover is built for this job.

Damage in an index

Here the damaged page belongs to the index idx_users_email. REINDEX can rebuild an index whose pages are readable, but it can’t get past a page of junk. .recover rebuilds the index from the table:

$ cp app-good.db idx.db
$ python3 corrupt.py idx.db 7
overwrote page 7 of idx.db
$ sqlite3 idx.db 'PRAGMA integrity_check;' | head -n 3
Error: stepping, database disk image is malformed (11)
*** in database main ***
Tree 3 page 7: btreeInitPage() returns error code 11
wrong # of entries in index idx_users_email
$ sqlite3 idx.db 'REINDEX idx_users_email; PRAGMA integrity_check;'
Error: stepping, database disk image is malformed (11)
$ sqlite3 idx.db .recover | sqlite3 idx-recovered.db > /dev/null
$ sqlite3 idx-recovered.db 'PRAGMA integrity_check; SELECT count(*) FROM users;'
ok
2000

“file is not a database” is a different error

If the first bytes of the file are damaged, SQLite can’t recognise it as a database at all. That’s SQLITE_NOTADB (26), “file is not a database”:

$ cp app-good.db header.db
$ python3 -c "f = open('header.db', 'r+b'); f.write(b'not a database!!'); f.close()"
$ sqlite3 header.db 'SELECT count(*) FROM users;'
Error: in prepare, file is not a database (26)

.recover needs a readable header, so a backup is the main way back from this one. Also check that the path points at the right file.

Edge cases

  • WAL databases. The latest changes may still be in the -wal file. Copy it with the database, and run the checks on the copy with both files together.
  • Keeps happening on the same machine. Repeated corruption points to the causes listed above. SQLite’s How To Corrupt page is the checklist.

Conclusion

Copy the files first, run PRAGMA integrity_check to see what’s damaged, and rebuild with .recover into a new file. Check the result with integrity_check again before you use it. Keep backups, because .recover gets back what’s readable and nothing more.

Further reading

Tested with SQLite 3.51.0 and Python 3.9.6 on 7 October 2026.