sitekits.dev
har

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

§01 FIELD GUIDE

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 -1not available, not zero — including bodySize, pageTimings and every timing phase.

The seven timing phases

Milliseconds, in the order they occur.

PhaseMeasuresA large value usually means-1 when
blockedQueueing before the request leaves the clientHTTP/1.1’s ~6-per-origin cap; proxy negotiationNever queued
dnsName resolution for the hostCold resolver, long CNAME chainConnection reused
connectTCP handshake — includes sslDistant origin, high RTT, no keep-aliveConnection reused
sslTLS negotiation (counted inside connect)Large cert chain, no session resumptionPlain HTTP, or reuse
sendPushing request bytes outA large upload body
waitTTFB after the last request byteBackend processing plus one RTT
receiveReading the body off the wireUncompressed 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.

FAQ
Why doesn't the total time in my HAR match the page load time I measured?
Because requests overlap. Adding up entry.time across log.entries[] double-counts every millisecond in which two requests were in flight, so the sum is usually several times wall-clock. For wall clock, read log.pages[].pageTimings.onLoad, or take the span from the earliest startedDateTime to the latest startedDateTime plus its time.
What does a long wait phase in a HAR entry actually mean?
wait is time-to-first-byte measured after the last request byte was sent, so it covers the server's own processing plus one network round trip. A fat wait with a thin receive points at the backend — a slow query, a cold cache, a cross-region hop. The opposite shape, thin wait and fat receive, means the response was simply large or the link was slow.
I redacted the Cookie headers in my HAR — why are the session cookies still in the file?
Because HAR stores cookies twice. request.headers[] holds the raw Cookie line, while request.cookies[] and response.cookies[] hold the same values parsed into name/value objects, so any pass that only walks headers leaves the jar intact — including this site's HAR File Sanitizer, which redacts header values, query and POST parameters and both bodies, but does not touch the cookies[] arrays. Search the file for "cookies" before you attach it, and check serverIPAddress and any secret sitting in a URL path segment while you are there.
Why do some HAR entries show dns -1 and connect -1?
Those phases did not happen for that request. In HAR 1.2, -1 means the value is not applicable or not available, which is what you get when the request rode an existing keep-alive connection and so needed no DNS lookup, TCP handshake, or TLS negotiation. Treating -1 as zero is harmless; averaging it as if it were a real measurement is not.
Why are the request and response bodies missing from my HAR?
postData.text and content.text are optional fields, and DevTools routinely omits large or binary payloads to keep the export manageable. When a body is present it may be Base64 rather than plain text, flagged by content.encoding set to base64. An absent body means the exporter did not record it, not that the response was empty.