browserforensics
Back to all articles

Recovering recent history from SQLite WAL files

2026-05-19 · 2 min

Chrome and Firefox both open their main databases in WAL mode (PRAGMA journal_mode=WAL). Transactions append to <db>-wal first; SQLite later checkpoints them into the main .db / .sqlite. If you copy only the main file you can silently lose the most recent visits, downloads and cookies — frequently exactly the activity an investigation is about.

Why this is the highest-value sidecar

The WAL contains committed transactions. They are not partial, they are not provisional, they are just not yet merged. Browsers checkpoint on size thresholds (default 1 MB), at clean shutdown, and on explicit PRAGMA wal_checkpoint. A running Chrome session can hold hours of visits in the WAL.

A clean shutdown empties the WAL into the main file. A crash, an acquisition during use, or pulling power leaves the WAL populated.

What to do during acquisition

  • Always collect the -wal and -shm sidecars alongside the main DB.
  • Keep them together and with matching base names. SQLite will only attach the WAL whose name matches the database.
  • Do not let any tool open the database read-write before you copy. A rogue sqlite3 history that opens read-write triggers a checkpoint and reshuffles the WAL.

Reading uncommitted-looking data

A WAL frame stream is a sequence of 24-byte frame headers plus full pages. Each frame names the page number it overwrites and the salt of its commit. Open the matched DB + WAL with a recent SQLite library and the merge is automatic. If you need to recover deleted rows (rows that existed in older WAL frames but were later overwritten), you have two options:

  1. WAL frame carving. Walk the WAL outside SQLite, dump every frame for every page, and reconstruct the per-page history. Tools like Sanderson's walitean and Sleuth Kit's SQLite plugins do this.
  2. Page-level recovery from the freelist. Even checkpointed WAL frames may have written deleted-row data into unallocated cells. A raw page walker over the merged DB recovers the freelist content, which often holds the deleted history rows verbatim.

Practical pitfalls

  • Firefox's places.sqlite-wal and Chrome's History-wal follow the same rules. The mistake repeats across both.
  • If the user ran "Clear browsing history" recently, the rows are deleted but the underlying pages may still hold them until SQLite reuses the cells. WAL frame analysis sometimes recovers what looked permanently gone.
  • Mounted forensic images preserve the sidecars. Live triage scripts often do not.

Further reading