Recommended Free Tools
Use Puppeteer’s Chrome DevTools Protocol (CDP) session to capture Network events: enable the Network domain before navigation, then correlate request, response, and completion events by requestId. Save response.timing for phase timings such as DNS, connection, TLS, and response headers; calculate total elapsed time from the request and completion timestamps.
What you need to collect the Network timings
Puppeteer’s page-attached CDP session is the route to Chrome’s lower-level Network telemetry. The Network domain exposes request and response timing and lifecycle events (Chrome DevTools Protocol: Network). Puppeteer documents page.createCDPSession() for attaching that session (Puppeteer Page.createCDPSession) and CDPSession.send() for sending protocol commands (Puppeteer CDPSession.send).
- Install Node.js and Puppeteer in a project that can launch Chrome or Chromium.
- Enable
Networkbefore navigating; otherwise requests that begin before the listener is active will be missed. - Use
requestIdto join each request’s events. Do not match records only by URL: the same URL can be requested more than once.
The code below is an illustrative implementation of the documented API and protocol events; it has not been executed here. Pin your Puppeteer version in a real project and record the browser and protocol versions alongside results when comparing runs.
Capture request timing data with Puppeteer
Save this as an ES module such as timings.mjs, install Puppeteer with npm install puppeteer, then run node timings.mjs https://example.com. It logs a JSON record when each request completes or fails.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →#1 Best Overall
import puppeteer from 'puppeteer';
const target = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
const cdp = await page.createCDPSession();
const requests = new Map();
cdp.on('Network.requestWillBeSent', event => {
// A redirect can reuse requestId. Close the previous leg before storing
// the new request, so each redirect hop has its own record.
if (event.redirectResponse) {
const previous = requests.get(event.requestId);
if (previous) {
previous.redirectResponse = event.redirectResponse;
previous.redirected = true;
console.log(JSON.stringify(previous));
}
}
requests.set(event.requestId, {
requestId: event.requestId,
url: event.request.url,
method: event.request.method,
type: event.type,
startedAt: event.timestamp,
wallTime: event.wallTime,
redirectResponse: event.redirectResponse ?? null,
});
});
cdp.on('Network.responseReceived', event => {
const record = requests.get(event.requestId);
if (!record) return;
record.status = event.response.status;
record.mimeType = event.response.mimeType;
record.fromDiskCache = event.response.fromDiskCache;
record.fromServiceWorker = event.response.fromServiceWorker;
record.timing = event.response.timing ?? null;
});
cdp.on('Network.loadingFinished', event => {
const record = requests.get(event.requestId);
if (!record) return;
record.finishedAt = event.timestamp;
record.totalMs = (event.timestamp - record.startedAt) * 1000;
record.encodedDataLength = event.encodedDataLength;
console.log(JSON.stringify(record));
requests.delete(event.requestId);
});
cdp.on('Network.loadingFailed', event => {
const record = requests.get(event.requestId);
if (!record) return;
record.failed = true;
record.errorText = event.errorText;
record.canceled = event.canceled ?? false;
record.blockedReason = event.blockedReason ?? null;
console.log(JSON.stringify(record));
requests.delete(event.requestId);
});
await cdp.send('Network.enable');
await page.goto(target, { waitUntil: 'networkidle0' });
} finally {
await browser.close();
}
networkidle0 asks Puppeteer to wait for network activity to become idle, but some pages keep connections open or continually make requests. If that makes navigation wait indefinitely or longer than appropriate for your task, use a different documented page.goto() wait condition or a timeout strategy suitable for the page. The event handlers still observe requests after Network is enabled.
What each handler records
Network.requestWillBeSentstarts the record, including URL, method, resource type, and a monotonic timestamp.Network.responseReceivedadds response status and MIME type, plusresponse.timingwhen Chrome supplies it.Network.loadingFinishedrecords completion and encoded response size. Its timestamp minus the request timestamp, multiplied by 1,000, gives total elapsed milliseconds.Network.loadingFailedretains failures instead of silently dropping them. A failed network load and an HTTP error response are not the same: HTTP 404 or 503 responses can complete successfully at the HTTP transport level.
Redirects need separate legs
For a redirect, Chrome reports the prior response on a subsequent requestWillBeSent event, and the request continues as a new leg. The example emits the previous record when that happens, then starts a fresh record for the next URL. A full waterfall should show each leg separately rather than treating a redirect chain as one request (Puppeteer HTTPRequest lifecycle).
The redirect response is attached to the prior leg in this example. If your report needs every redirect response’s status, headers, and timing fields in the same schema as ordinary responseReceived events, enrich that leg from event.redirectResponse before emitting it.
Read the timing fields correctly
response.timing is not a set of independent durations. Its requestTime is a baseline in seconds; most phase values are millisecond offsets relative to that baseline. For a phase, subtract its start offset from its end offset. For example, dnsEnd - dnsStart is the DNS phase duration when both values are available. The protocol defines the fields and their meanings (Network.ResourceTiming).
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
| Fields | What they describe | How to use them |
|---|---|---|
proxyStart, proxyEnd |
Proxy-related phase, when applicable. | Subtract start from end only when both are meaningful. |
dnsStart, dnsEnd |
DNS lookup phase. | dnsEnd - dnsStart. |
connectStart, connectEnd |
Connection establishment phase. | connectEnd - connectStart. |
sslStart, sslEnd |
TLS handshake phase, when applicable. | sslEnd - sslStart. |
sendStart, sendEnd |
Sending the request. | sendEnd - sendStart. |
receiveHeadersStart, receiveHeadersEnd |
Receiving response headers. | The start is useful as a protocol-level waiting/TTFB measurement; the pair also shows the header-receive interval. |
Worker-related timing fields may also appear. Preserve missing or negative values rather than converting them to zero: cache hits, reused connections, proxies, service workers, and protocol behavior can make some phases inapplicable or unavailable. A zero means a measured zero-duration phase; a missing value means the phase was not supplied, which is materially different.
Relate CDP measurements to the DevTools Timing tab
Chrome DevTools’ Network panel presents a request list, waterfall, total duration, latency, and a Timing tab. The Timing view groups work into request sent, waiting (TTFB), and content download; content download is the time spent reading the response body (Chrome DevTools Network reference). CDP exposes machine-readable events and timing fields underlying these measurements, while the UI’s labels are presentation groupings. The panel records requests while DevTools is open, and selecting a request exposes its Timing tab (Chrome DevTools Network panel reference).
Do not assume one CDP field will have an identical label or boundary to every UI grouping. Keep the raw fields in your output, document your derived calculations, and compare the same request and run conditions.
Choose which traffic belongs in your report
A page can generate more than its main document request. Depending on the question, you may want all observed traffic or a narrower slice. Record event.type and filter later, or filter as events arrive by type, URL, or frame. Decide explicitly whether the report includes:
Free tools Windows power users keep installed
One-click scans. No signup required.
- Cached responses and service-worker responses, which may not have ordinary network phases.
- Preflight requests, worker traffic, and WebSocket activity.
- Third-party scripts, analytics, images, and other subresources in addition to the document.
For reproducible comparisons, record the target URL, browser version, Puppeteer version, protocol version, run date, and relevant network conditions that you configured. A page’s timing changes with cache state, connection reuse, geographic location, server behavior, and background activity; a single capture is not a universal performance benchmark.
Handle extra-info events without assuming an order
Network.responseReceivedExtraInfo can provide additional response details, but it is not emitted for every request and can arrive before or after responseReceived. If you consume it, buffer it by requestId and merge when the matching record exists rather than relying on event order (Chrome DevTools Protocol: Network events).
The core example does not require extra-info events, which keeps the timing capture simpler. Add them only when you need their additional response data and can tolerate missing or reordered events.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting common problems
No requests appear before navigation
Confirm that await cdp.send('Network.enable') completes before page.goto(), and attach listeners before navigating. A request already in progress when monitoring starts may not have a matching start record.
Rank #4
A record has no timing object
Keep timing: null when response.timing is absent. Do not infer DNS, TLS, or TTFB values from unrelated fields. The timing object is available only on responses that carry timing data.
Some phase values are missing or negative
This can be legitimate for cached responses, reused connections, proxies, service workers, or phases that do not apply. Preserve the raw values and avoid reporting a computed duration unless both endpoints are valid for that phase.
The request seems to disappear on HTTP 404 or 503
Check the loadingFinished handler, not just loadingFailed. HTTP error statuses still represent received responses and can finish normally; reserve the failed path for a loading failure and retain the status from responseReceived.
Redirect chains show only the final URL
Handle redirectResponse on requestWillBeSent before replacing the record for that request ID. Emit the previous leg, then start a new record. This preserves the waterfall sequence instead of collapsing the chain.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallNavigation never reaches network idle
Some sites poll, stream, or keep connections open. Choose a navigation wait condition aligned with your capture goal rather than assuming all pages become idle. You can collect Network events while the page is active, then apply a bounded capture duration in your own script if the workload requires one.
Results differ between runs
Check that browser and Puppeteer versions, cache state, network conditions, target page state, and the inclusion filters are consistent. Keep raw timestamps and protocol fields so you can distinguish capture differences from changes introduced by your calculations.
Or skip the browser setup
If you need a clean page screenshot rather than a machine-readable request waterfall, ScreenshotNeo takes a screenshot or PDF from one GET request. Its capture can accept cookie or consent banners and remove 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, with the response identifying the page verdict and billing status in headers. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.
Here is the cURL form; replace the example URL with the page you want to capture. See the ScreenshotNeo API documentation for the request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. These are screenshot captures, not a substitute for Puppeteer CDP timing records.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




