Chrome History database forensics
2026-06-21 · 5 min
The History database is the single richest timeline source in a Chromium
profile. It is not just a list of URLs — it records every individual visit
with microsecond timing, how the user arrived at each page, the referrer
chain, and the queries typed into the omnibox. Read the schema properly and
you reconstruct intent, not just presence.
Where it lives
History is a SQLite database at …\User Data\<profile>\History. Edge
uses the same filename and the same schema. There is no extension. The
profile is Default, Profile 1, Profile 2, etc. — enumerate them all.
Always grab the -wal sidecar with it; recent visits live there until a
checkpoint merges them. See
recovering recent history from the WAL
and Chrome history file locations.
The two core tables
The urls table is one row per distinct URL; the visits table is one row
per visit event. Join visits.url to urls.id.
urls
| Column | Meaning |
|---|---|
id | Primary key, referenced by visits.url |
url | The full URL |
title | Last page title seen |
visit_count | Total visits to this URL |
typed_count | Times the URL was typed (not clicked) |
last_visit_time | Most recent visit (WebKit epoch) |
hidden | 1 = excluded from autocomplete UI |
A high typed_count is a strong signal of deliberate navigation — the user
knew the address. hidden rows still happened; they are just suppressed in
the address bar, not deleted.
visits
| Column | Meaning |
|---|---|
id | Visit primary key |
url | FK to urls.id |
visit_time | When the visit occurred (WebKit epoch) |
visit_duration | Dwell time in microseconds |
transition | Packed transition type + qualifier bits |
from_visit | visits.id of the referring visit |
opener_visit | Visit that opened this one (e.g. new tab) |
from_visit is the referrer chain. Walk it backwards to reconstruct how a
user got from a search results page to a specific destination, click by
click. opener_visit links a page to the tab that spawned it.
Decoding the transition field
transition is an integer that packs two things. The core type is the
low byte — mask with 0xff. The upper bits are qualifier flags.
Core type (& 0xff) | Value | Meaning |
|---|---|---|
LINK | 0 | Clicked a link |
TYPED | 1 | Typed in the address bar |
AUTO_BOOKMARK | 2 | Opened from a bookmark / UI suggestion |
AUTO_SUBFRAME | 3 | Subframe loaded automatically |
MANUAL_SUBFRAME | 4 | Subframe loaded by user action |
GENERATED | 5 | Omnibox search / generated URL |
FORM_SUBMIT | 7 | Submitted a form |
RELOAD | 8 | Reloaded the page |
KEYWORD | 9 | Keyword/custom-search-engine navigation |
Common qualifier bits set in the upper bytes:
| Qualifier | Mask | Meaning |
|---|---|---|
CHAIN_START | 0x10000000 | First visit in a redirect chain |
CHAIN_END | 0x20000000 | Last visit in a redirect chain |
CLIENT_REDIRECT | 0x40000000 | JS / meta-refresh redirect |
SERVER_REDIRECT | 0x80000000 | HTTP 3xx redirect |
So TYPED with no qualifiers is a clean manual navigation. A LINK with
SERVER_REDIRECT and no CHAIN_START is an intermediate hop, not a real
destination — do not present it as a page the user chose to visit. The
AUTO_SUBFRAME rows are almost always ads and trackers; filter them out of
a timeline unless framing matters.
Omnibox searches
keyword_search_terms records queries typed into the address bar that
resolved to a search. It has term (the literal query string) and a
url_id joining to urls.id. This recovers the search text — "how to
delete browser history", a name, an address — independent of the search
engine's own logs. It is one of the highest-value tables in the database.
Supporting tables
segments/segment_usage— aggregate per-URL visit stats that power the "most visited" tiles. Useful for ranking habitual sites.visit_source— flags whether a visit was synced from another device or local. Critical when arguing a visit happened on this machine versus arriving via Chrome Sync.downloads/downloads_url_chains— download records live in the same database. Covered separately in Chrome download history forensics.
Timestamps
Every time column (last_visit_time, visit_time) is the WebKit/Chrome
epoch: microseconds since 1601-01-01 UTC. visit_duration is also in
microseconds, but it is an elapsed interval, not an epoch value. Convert
correctly or your timeline drifts by centuries — see
browser timestamp formats.
Pitfalls
- A populated
-walcan hold the entire current browsing session. Open the DB and WAL together or you report a stalelast_visit_time. visit_countand the actual count ofvisitsrows can disagree after a partial history clear — Chrome may delete visit rows but leave theurlsaggregate behind. Cross-check both.- Redirect chains inflate the apparent number of "visited" sites. Always
resolve
CHAIN_START/CHAIN_ENDbefore counting destinations. - A deleted URL removes the
urlsrow and itsvisits, but pages in the freelist and WAL frames often still hold them.
The tool parses urls, visits (with transitions decoded into core type
plus qualifiers), keyword_search_terms and downloads, applies the WAL
automatically, and renders timestamps in the WebKit epoch — entirely in
your browser, with nothing uploaded.