Firefox session restore: jsonlz4 forensics
2026-06-21 · 3 min
Firefox keeps a running record of what was open so it can restore your
windows after a crash or restart. That same record is a forensic gift: it
holds the URLs of currently open tabs, their per-tab navigation history,
and a list of recently closed tabs with timestamps — often surviving long
after places.sqlite has been cleared.
The files
Session state lives in the profile root and in sessionstore-backups/:
| File | When written |
|---|---|
sessionstore.jsonlz4 | On clean shutdown (open tabs at exit) |
sessionstore-backups/recovery.jsonlz4 | Live state, written every ~15s while running |
sessionstore-backups/recovery.baklz4 | Previous copy of recovery.jsonlz4 |
sessionstore-backups/previous.jsonlz4 | Session before the current one |
sessionstore-backups/upgrade.jsonlz4-* | Snapshot taken at a version upgrade |
recovery.jsonlz4 is the one that matters on a live or crashed machine —
it reflects what was open at the moment of acquisition. On a cleanly shut
down host, look to sessionstore.jsonlz4 instead. The upgrade.jsonlz4-*
files are easy to overlook and can carry tabs from weeks earlier.
The mozLz4 container
These are not standard LZ4 files. Firefox wraps a single LZ4 block in a small Mozilla-specific header it calls mozLz4:
offset 0 8 bytes magic "mozLz40\0"
offset 8 4 bytes decompressed size, uint32 little-endian
offset 12 … one LZ4 block (raw, not framed)
Generic lz4 tooling chokes on the magic. Read the 12-byte header, take
the declared decompressed size, then run a raw LZ4 block decompression
over the remainder. The result is plain UTF-8 JSON. The tool does exactly
this in the browser — it strips the mozLz40\0 magic, reads the size, and
inflates the block client-side.
The JSON shape
Decompressed, the structure is a tree of windows and tabs:
{
"windows": [
{
"tabs": [
{
"entries": [
{ "url": "https://example.com/", "title": "Example" },
{ "url": "https://example.com/login", "title": "Sign in" }
],
"index": 2,
"lastAccessed": 1718900000000
}
],
"_closedTabs": [
{ "state": { /* … */ }, "title": "Old page", "closedAt": 1718890000000 }
]
}
]
}
Read it like this:
entriesis the per-tab navigation history — the back/forward stack. Every URL the tab walked through is here, not just the current page.indexis 1-based and points at the entry currently displayed. Entries afterindexare the forward history.lastAccessedon each open tab is the last time it was focused._closedTabsis the recently closed list (the "Recently Closed Tabs" menu). Each holds a nestedstatewith its ownentries, plus aclosedAt.
Timestamps
lastAccessed and closedAt are Unix milliseconds — not the PRTime
microseconds Firefox uses in places.sqlite. Convert by dividing by 1000
before treating them as epoch seconds, or you will land tens of thousands
of years in the future. See
browser timestamp formats for the
wider zoo of Firefox time bases.
What to recover
- Open tabs at acquisition — the full URL list per window, with focus
times. Pulls from
recovery.jsonlz4(running) orsessionstore.jsonlz4(clean exit). - Per-tab back/forward history —
entriesoften shows the path a user took into a site, including intermediate pages history collapsed away. - Recently closed tabs —
_closedTabswithclosedAt. These are tabs the user deliberately closed, which is frequently the interesting set.
The tool decompresses each mozLz4 file and parses open and recently-closed
tabs into a single timeline you can sort by lastAccessed/closedAt.
Pitfalls
- Compare
recovery.jsonlz4againstsessionstore.jsonlz4. A clean shutdown updates the latter; a crash leaves it stale whilerecoveryholds the truth. - Don't stop at the current page — mine
entriesfor the navigation history hiding inside each tab. - A blank
_closedTabslist does not mean nothing was closed; Firefox caps the kept count and the user may have cleared it.