ScreenshotNeo

BlogHow-to

How to Measure Response Timing in Puppeteer

Measure the interval from a Puppeteer action to a matching response, inspect browser resource timing, and distinguish response receipt from full download time.

By the ScreenshotNeo team4 October 20268 min read

To measure the time from a Puppeteer action until a particular response arrives, install page.waitForResponse() before triggering the request, start a monotonic stopwatch at the exact boundary you want to measure, then stop it when the matching response is returned. Call the result action-to-response time: it is not pure server processing time.

Puppeteer exposes several different boundaries: request issuance, response receipt, body download completion, and request failure. Choose the one that answers your question. For browser-reported resource timing, inspect HTTPResponse.timing(); it is a separate measurement and may be null. [Page.waitForResponse()] [HTTPResponse.timing()] [HTTPRequest lifecycle]

1. Measure an action through response receipt

This runnable example measures from just before filling and submitting a search form through receipt of the matching GET response. Adjust the selectors, URL fragment, method, and action boundary to match your application. It uses the Puppeteer package and assumes a page has already been created.

import puppeteer from 'puppeteer';
import { performance } from 'node:perf_hooks';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // Register the waiter before the action so a fast response cannot be missed.
  const responsePromise = page.waitForResponse(
    response => response.url().includes('/api/search') &&
      response.request().method() === 'GET',
    { timeout: 30_000 }
  );

  // This boundary includes both filling and clicking.
  const startedAt = performance.now();
  await page.locator('input[name="q"]').fill('puppeteer');
  await page.locator('button[type="submit"]').click();

  const response = await responsePromise;
  const actionToResponseMs = performance.now() - startedAt;

  console.log({
    actionToResponseMs,
    url: response.url(),
    status: response.status(),
    ok: response.ok(),
    resourceTiming: response.timing(),
    fromCache: response.fromCache(),
    fromServiceWorker: response.fromServiceWorker(),
  });
} finally {
  await browser.close();
}

The timer begins before the field is filled, so the measured interval includes that fill, the click, browser-side work, and the path to response receipt. If you want click-to-response time, move startedAt to immediately before the click. Use performance.now(), a monotonic clock suitable for elapsed intervals, rather than subtracting wall-clock timestamps.

2. Match the response you intend to measure

A page can issue many requests for images, analytics, polling, prefetching, and application data. A broad match can return the wrong response. Match stable request properties, especially URL path and HTTP method; include query parameters or request details when needed to distinguish concurrent calls.

const responsePromise = page.waitForResponse(response => {
  const request = response.request();
  const url = new URL(response.url());
  return url.pathname === '/api/search' &&
    url.searchParams.get('q') === 'puppeteer' &&
    request.method() === 'GET';
});

await page.locator('button[type="submit"]').click();
const response = await responsePromise;

waitForResponse() accepts a URL or a predicate and resolves to the matching HTTPResponse. Register it before the action that triggers the request. Its documented default timeout is 30 seconds; set a different timeout in the options or change the page default with page.setDefaultTimeout(). An AbortSignal can cancel the wait. See the Puppeteer API reference for the installed release’s exact signatures.

3. Pick the right timing boundary

Question Measurement What it includes
How long from a test action until the response is received? Monotonic stopwatch around the action and waitForResponse() Action execution and the path up to response receipt. This is not server-only latency.
What resource timing did the browser report? response.timing() Browser-reported resource timing data; it can be null.
How long until the response body finishes downloading? Measure through the matching request’s requestfinished event Request completion after body download.
Did the request fail at the network level? Observe requestfailed A failed request; distinguish this from an HTTP error status.

Puppeteer documents request issue, response receipt, completion, and failure as separate lifecycle events. A 404 or 503 response is still an HTTP transaction that can complete and emit requestfinished. Check response.status() or response.ok() separately when application success matters; ok() is true for 200–299 responses. [HTTPRequest lifecycle] [HTTPResponse]

Measure through body completion

When you need both response receipt and body completion, correlate the response’s request with its completion event and use one monotonic clock. The following pattern listens before the action and matches the request object returned by the response. As above, customize the predicate for your request.

import { performance } from 'node:perf_hooks';

const isTarget = response =>
  response.url().includes('/api/report') &&
  response.request().method() === 'GET';

const responsePromise = page.waitForResponse(isTarget);
const startedAt = performance.now();
await page.locator('button[data-action="load-report"]').click();
const response = await responsePromise;
const responseReceivedAt = performance.now();

const request = response.request();
const finishedPromise = new Promise((resolve, reject) => {
  const onFinished = finishedRequest => {
    if (finishedRequest === request) {
      cleanup();
      resolve(performance.now());
    }
  };
  const onFailed = failedRequest => {
    if (failedRequest === request) {
      cleanup();
      reject(new Error(`Request failed: ${failedRequest.url()}`));
    }
  };
  const cleanup = () => {
    page.off('requestfinished', onFinished);
    page.off('requestfailed', onFailed);
  };
  page.on('requestfinished', onFinished);
  page.on('requestfailed', onFailed);
});

const finishedAt = await finishedPromise;
console.log({
  actionToResponseMs: responseReceivedAt - startedAt,
  responseToFinishedMs: finishedAt - responseReceivedAt,
  actionToFinishedMs: finishedAt - startedAt,
  status: response.status(),
});

For requests that can finish before the listeners are installed, attach lifecycle listeners before triggering the action and record timestamps keyed by the request object. Also decide how your harness will handle redirects and failures before relying on completion metrics.

4. Interpret timing data and edge cases

Resource timing may be absent

HTTPResponse.timing() is documented as returning Protocol.Network.ResourceTiming | null. Guard for null and do not label the result as user-action latency. Use a separate stopwatch for the action-to-response interval. [HTTPResponse.timing()]

HTTP errors and network failures differ

An HTTP 404 or 503 has a response and status code; it is not necessarily a request failure in Puppeteer’s lifecycle. Network failures are represented separately. Report status and success criteria alongside timing so a quick error response is not mistaken for a successful operation.

Redirects create multiple request hops

A redirect finishes one request and issues another. A predicate may match the first hop or the final URL, depending on what it checks. Decide whether you are timing one HTTP hop or the complete logical navigation, then match and report that boundary explicitly. [HTTPRequest lifecycle]

Cache and service workers affect comparisons

Responses may come from browser cache or a service worker. Record response.fromCache() and response.fromServiceWorker() when comparing runs. Keep cache state, service-worker behavior, browser version, network, and CPU conditions consistent, or report those differences with the measurement. [HTTPResponse]

Use page metrics for diagnosis, not as a response timer

page.metrics() reports page-level metrics such as layout, style recalculation, script duration, and task duration. Those can help diagnose browser work around a slow interaction, but they do not replace per-response timing. The metrics timestamps use monotonic seconds from an arbitrary point in the past. [Page.metrics()]

5. Keep measurements reliable and useful

  1. Define the interval in the report. Name the start event and end event, such as click-to-response or submit-sequence-to-request-finished.
  2. Install waiters and listeners first. This avoids missing a fast response or completion event.
  3. Use a precise predicate. Match the endpoint and method, and distinguish query values or other stable request details if multiple calls are possible.
  4. Report outcome with duration. Include status, ok(), and whether the request failed. Do not treat an HTTP error as a network failure.
  5. Control or record cache behavior. Include cache and service-worker indicators in test output.
  6. Repeat under comparable conditions. Use the same browser, page state, network and CPU conditions. Report distributions across runs for performance work instead of drawing conclusions from a single sample.
  7. Use bounded timeouts and cleanup. Set a timeout that fits the test, handle rejection and cancellation, and remove event listeners after the matched request completes.

For cost, this measurement is local browser automation: the timing pattern itself has no API usage charge. The practical costs are the compute and time spent running browser instances and test repetitions. No benchmark or fixed runtime is implied here.

6. Troubleshooting

Symptom Likely cause Fix
waitForResponse() times out The action did not issue the request, the predicate is too narrow, or the request took longer than the timeout. Confirm the action and endpoint, log request URLs and methods, simplify the predicate temporarily, then set a suitable timeout.
The wrong response is returned Several requests satisfy a broad URL substring. Match pathname, method, and relevant query values; add other stable request criteria.
Elapsed time is unexpectedly large The stopwatch starts before setup work such as filling fields, or waits include unrelated action time. Move the start point to the intended event and label the measured interval accurately.
The response is fast but the page still appears slow The measurement ends at response receipt, while body download, script work, rendering, or another request continues. Measure request completion or a separate user-visible readiness condition; use page metrics to investigate browser work.
A 404/503 is reported as a completed request HTTP error status is not the same as a failed network request. Check status or ok() in addition to lifecycle outcome.
Timing changes between runs Cache, service worker, redirects, browser, CPU, or network conditions differ. Record cache and service-worker flags and hold or document the other conditions.
timing() is null Resource timing is unavailable for that response. Handle the nullable result and use the stopwatch when the question is elapsed action-to-response time.
Completion event is missed Listeners were attached after the request completed. Register lifecycle listeners before triggering the action and correlate by request object.

7. Or skip the browser setup

If you need a rendered page image or PDF rather than an instrumented Puppeteer test, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. The response also reports page verdict and billing status; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing.

For API details and the available capture options, see the ScreenshotNeo documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie banners and consent overlays, newsletter popups, and chat widgets are removed before the shot; each step can be turned off.
  • Bot checks, blank pages, and failed loads are never billed.
  • An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

8. FAQ

Can I call this server response time?

Only if you define that term for your own measurement. The stopwatch pattern includes browser-side action work and time to response receipt, so call it action-to-response time unless you separately measure server processing.

Does waitForResponse() mean the body has downloaded?

No. It resolves when the matching response is available. Use the request completion lifecycle boundary when you need the body download to finish.

What does response.ok() mean?

It indicates an HTTP status in the 200–299 range. Check it separately from whether the network request completed.

Which Puppeteer version should I use?

The cited API references for request and response timing and waitForResponse() identify version 25.12.0; the metrics reference is the Next documentation. Check the API documentation for the Puppeteer version installed in your project before relying on version-specific signatures.