HAR tools
A HAR file is a JSON transcript of everything your browser saw on the network: how it is structured, what its seven timing phases mean, and what to redact before sharing one.
2 tools
A HAR is a transcript, not a packet capture
A HAR (HTTP Archive) file is one JSON document describing what a browser
observed on the network. Everything interesting lives in log.entries[]: one
element per request/response pair, carrying method, URL, headers, cookies,
sizes, a phase-by-phase timing breakdown and — sometimes — the bodies. The
format is HAR 1.2, a W3C draft never ratified, yet Chrome, Firefox, Safari,
Charles and Fiddler emit dialects close enough that files move between them.
The mental model that saves you hours: the file is written by the browser’s own network stack, after the fact — it holds that stack’s summary, not the wire. No TLS handshake records, no HTTP/2 frames, no DNS packets, only durations the stack chose to report. Requests that fired before DevTools opened are missing, as is the previous navigation unless Preserve log was on, and cache or service-worker hits appear as entries that never touched the network. Two captures of “the same” bug routinely disagree.
Anatomy of the file
log
├─ version "1.2" · creator{name,version} · browser
├─ pages[] id, title, startedDateTime, pageTimings{onContentLoad,onLoad}
└─ entries[] pageref, startedDateTime, time, connection, serverIPAddress
├─ request method, url, httpVersion, headers[], cookies[],
│ queryString[], postData{mimeType,text,params}
├─ response status, statusText, redirectURL, headers[], cookies[],
│ content{size,compression,mimeType,text,encoding}
├─ cache beforeRequest, afterRequest
└─ timings blocked, dns, connect, ssl, send, wait, receive
Two details bite people. request.headers[] and request.cookies[] are
separate representations of the same cookies, so a scrub that only walks
headers leaves the jar intact. Keys beginning with _ (_initiator,
_priority, _resourceType, _webSocketMessages) are vendor extensions:
useful in Chrome, missing elsewhere. And nearly every numeric field may be
-1 — not available, not zero — including bodySize, pageTimings and
every timing phase.
The seven timing phases
Milliseconds, in the order they occur.
| Phase | Measures | A large value usually means | -1 when |
|---|---|---|---|
blocked | Queueing before the request leaves the client | HTTP/1.1’s ~6-per-origin cap; proxy negotiation | Never queued |
dns | Name resolution for the host | Cold resolver, long CNAME chain | Connection reused |
connect | TCP handshake — includes ssl | Distant origin, high RTT, no keep-alive | Connection reused |
ssl | TLS negotiation (counted inside connect) | Large cert chain, no session resumption | Plain HTTP, or reuse |
send | Pushing request bytes out | A large upload body | — |
wait | TTFB after the last request byte | Backend processing plus one RTT | — |
receive | Reading the body off the wire | Uncompressed or oversized payload | — |
entry.time is the total elapsed time, in practice
blocked + dns + connect + send + wait + receive. Because ssl is nested inside
connect, adding all seven columns double-counts the handshake — the most common
arithmetic error in HAR analysis.
Rebuilding the waterfall
Both axes are recoverable from the JSON: horizontal position is
entry.startedDateTime minus the owning log.pages[].startedDateTime; width is
entry.time, split into the phases above. entry.connection shows which
requests reused a socket.
Then read the shape, not the numbers. A staircase — each request starting as the
previous ends — is a dependency chain, HTML → JS → API → image, and no server
tuning fixes it; the discovery has to move earlier. A dense block whose
blocked segment grows further down the list is client-side queueing, which is
why HTTP/2 often flattens a waterfall without any response getting faster.
Picking the right tool
Paste the capture into HAR Viewer first — also on the
SRE shortlist. It flattens log.entries[] into
Method / Status / Type / Size / Time / URL with an
N requests · X KB · Y ms total summary, colouring the Status cell red for
4xx/5xx and orange for 3xx: the fastest way to find the outlier. Metadata
only, never bodies.
The viewer gives per-entry totals, not the phase split. For phases, query the
raw file with JSONPath Finder — $..timings for every
breakdown, or $.log.entries[?(@.time>1000)].request.url for just the slow ones.
If the export will not parse (truncated download, line-wrapped paste), run it
through JSON Formatter for the parser’s exact error position.
For the Status column, HTTP Status Codes turns a bare number
into a name, class and one-line meaning, filterable by code or keyword — the
quick way to settle 400 against 422. It stops at meaning, not redirect
behaviour: its 301 row says only that the resource moved permanently. Reading
a chain, add this from the protocol itself — 301 and 302 let a client
rewrite POST into GET and drop the body, while 307 and 308 do not. The
full comparison is the redirect matrix in the HTTP toolset, which
also covers the header side. With no capture, only a URL,
HTTP Headers fetches it server-side from
api.sitekits.dev and returns status, redirect hop count and all response
headers — body never retrieved, URL never stored. Decode a 401’s bearer token
with JWT Decoder to see whether exp had passed; replay it with
REST API Tester, which sends from your browser straight to
the target, so CORS applies as in your app.
Gotchas
A raw HAR is a credential
Session cookies, Authorization: Bearer …, x-api-key, signed URLs, login
bodies and every response your session could read are in there.
HAR Sanitizer runs entirely in your browser — nothing is
uploaded — replacing sensitive headers, parameters matching
token|key|secret|password|passwd|pwd|auth|session|sig|signature, and whole
postData.text / content.text values with [REDACTED], and reporting how many
it caught. Do it even if your browser offered a “sanitized” export — what those
strip varies by version.
Pattern redaction is not proof
Header names are matched against a fixed list, parameter names against a regex,
so anything unconventional survives: secrets in URL path segments
(/v1/reset/9f3c…), the structured cookies[] arrays, serverIPAddress (your
origin’s real IP), internal hostnames. Skim the output, paste any suspicious URL
into URL Parser for a decoded query string, and grep for
authorization and set-cookie before the file leaves your machine. Same reflex
across the privacy tools and the
security shortlist.
content.size and bodySize measure different things
content.size is the decoded body length; bodySize is the bytes actually
received, with content.compression recording the saving. Cached responses
report bodySize: 0, so a KB total from content.size overstates a compressed
site’s network cost.