How to Wait for a Response in Puppeteer
Use page.waitForResponse() to catch the network response caused by an action. Learn how to match it reliably, handle timeouts and errors, and read its body.
Use Puppeteer’s page.waitForResponse(urlOrPredicate, options) to wait for a matching network response. Start the wait before the click, form submission, or other action that triggers the request, then await the response promise. The method resolves to an HTTPResponse; it does not by itself tell you whether your application operation succeeded.
const responsePromise = page.waitForResponse(response =>
response.url() === 'https://example.com/api/data' && response.status() === 200
);
await page.locator('button.load-data').click();
const response = await responsePromise;
const body = await response.json();
console.log(body);
The official API describes the result as a “Promise which resolves to the matched response.” See Puppeteer’s Page.waitForResponse() reference.
1. Install Puppeteer and run a complete example
The example below opens a page, registers a response wait, clicks a button, checks the returned HTTP status, reads JSON, and closes the browser even if an error occurs.
npm install puppeteer
Save this as wait-for-response.js and run it with node wait-for-response.js. Replace the example page, button selector, and endpoint with those from your application.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const responsePromise = page.waitForResponse(
response => response.url() === 'https://example.com/api/data',
{ timeout: 10_000 }
);
await page.locator('button.load-data').click();
const response = await responsePromise;
if (!response.ok()) {
throw new Error(`API returned HTTP ${response.status()} for ${response.url()}`);
}
const data = await response.json();
console.log(data);
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
In modern Puppeteer, locators are the recommended way to select and interact with elements. They wait for an element to be present and in a suitable state for the action. The response wait is still a separate promise: the locator handles the click readiness, while waitForResponse() handles the network event. See the Puppeteer page interactions guide.
2. Register the wait before the action
The order matters. If you click first and install the response wait afterward, a fast request may already have completed. Create the promise first, trigger the action second, and await the promise third:
const responsePromise = page.waitForResponse(predicate);
await page.locator('button').click();
const response = await responsePromise;
This works because calling waitForResponse() starts the asynchronous wait immediately; storing its promise does not block the script. The action can proceed while Puppeteer listens for the matching response.
For a form submit, use the same pattern:
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/search') &&
response.request().method() === 'POST'
);
await page.locator('form button[type="submit"]').click();
const response = await responsePromise;
console.log(response.status());
3. Match the response you actually need
Pass a URL string when exactly one response can match it. Use a predicate when you need to distinguish repeated endpoints, HTTP methods, query strings, status codes, or other response properties. The predicate may be asynchronous.
Match a URL and status
const response = await page.waitForResponse(response =>
response.url() === 'https://example.com/api/data' &&
response.status() === 200
);
Match a POST request to an endpoint
const responsePromise = page.waitForResponse(response => {
const request = response.request();
const url = new URL(response.url());
return url.origin === 'https://example.com' &&
url.pathname === '/api/search' &&
request.method() === 'POST';
});
await page.locator('button.search').click();
const response = await responsePromise;
Parsing the URL avoids accidental matches such as /api/search-history when you intended the exact /api/search path. If query parameters identify the operation, inspect them with url.searchParams.
Inspect response content in an asynchronous predicate
Puppeteer supports an asynchronous predicate, including one that reads the response body. Use this when the URL alone cannot identify the response. Reading bodies while matching can consume extra time, so prefer URL, method, and status checks when they are sufficient.
const responsePromise = page.waitForResponse(async response => {
if (!response.url().includes('/api/results')) return false;
if (response.status() !== 200) return false;
const text = await response.text();
return text.includes('"complete":true');
});
await page.locator('button.run').click();
const response = await responsePromise;
Distinguish receipt from success
A response arriving does not mean the operation succeeded. HTTP responses such as 404 and 503 are still responses in the request lifecycle. If your next step requires success, check response.ok() or explicitly match an expected status, then handle failures deliberately.
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/data')
);
await page.locator('button.load-data').click();
const response = await responsePromise;
if (!response.ok()) {
throw new Error(`Request failed with HTTP ${response.status()}`);
}
const result = await response.json();
4. Read the response
After the wait resolves, the returned HTTPResponse exposes the URL, status, success indicator, request, headers, and response body methods. Choose the body reader that matches the server’s response format.
const response = await responsePromise;
console.log(response.url());
console.log(response.status());
console.log(response.ok());
console.log(response.headers());
const json = await response.json();
// Or: const text = await response.text();
// Or: const buffer = await response.buffer();
Use json() for JSON, text() for textual content, and buffer() for binary data. Do not call multiple body-reading methods on the same response unless you have verified the behavior you need; read the body once and retain the parsed value.
5. Set timeouts and cancel waits
waitForResponse() defaults to a 30-second timeout. Set a per-wait timeout in milliseconds to suit the operation. A timeout of 0 disables the timeout, and the page’s default timeout can change the default. The wait also accepts an AbortSignal for cancellation. See the method reference and wait timeout options.
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/data'),
{ timeout: 10_000 }
);
Use finite timeouts for ordinary automation so a missing action or incorrect matcher cannot hang a run. To cancel a wait when another condition ends the task:
const controller = new AbortController();
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/data'),
{ timeout: 15_000, signal: controller.signal }
);
// If your surrounding workflow decides the wait is no longer needed:
// controller.abort();
Catch timeout and abort errors at the workflow boundary if they are expected outcomes. Avoid disabling the timeout as a workaround for a predicate that does not match.
6. Choose the wait API for the condition
| What you need to observe | Use | What it gives you |
|---|---|---|
| A matching network response arrived | page.waitForResponse() |
The matching HTTPResponse |
| The page issued a matching request | page.waitForRequest() |
The matching request |
| An element appeared or changed visibility | page.waitForSelector() or a locator wait |
An element handle or completion of the locator condition |
| A custom page condition became true | page.waitForFunction() |
A handle for the function’s result |
| A navigation completed | A navigation wait such as page.waitForNavigation() |
The navigation response, if one exists |
Do not substitute a navigation wait for an XHR or fetch response when the action does not navigate. A page can update from an API response without navigating, and an API response can arrive before the corresponding DOM update.
Use waitForRequest() when issuance is the event you care about, not completion. Use waitForSelector() when the page’s visible or DOM state is the requirement. Use waitForFunction() when the needed condition is custom page state. Puppeteer documents these APIs in its selector, function, and interaction references.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
TimeoutError |
The trigger did not run, the response took longer than the timeout, or the predicate did not match the real response. | Confirm the click or submit occurred. Log observed URLs, methods, and statuses; then correct the predicate or choose a justified longer timeout. |
| The script misses a fast response | The action ran before the response wait was registered. | Create and store the waitForResponse() promise before triggering the action. |
| The wrong response matches | The predicate is broad, for example matching only a common path fragment. | Match the exact origin and pathname, and include method, query parameters, or status when relevant. |
| The wait resolves but the operation failed | The server returned an HTTP error status, which is still a received response. | Check response.ok() or the expected status, and handle the error body or status explicitly. |
| The response arrives but UI assertions fail | The application has not yet rendered the response into the DOM. | After awaiting the response, wait for the actual UI condition with a locator or selector. |
| The page navigates and the wrong event is awaited | The action’s key outcome is navigation rather than an in-page API response, or both happen. | Wait for the event the test needs. If both matter, register both waits before triggering the action and await both. |
| JSON parsing fails | The response is not valid JSON, is empty, or returned an error document. | Check status and content type, inspect text() for diagnosis, and only parse JSON when the body format warrants it. |
To diagnose a matcher, temporarily log network events around the action:
page.on('request', request => {
console.log('REQUEST', request.method(), request.url());
});
page.on('response', response => {
console.log('RESPONSE', response.status(), response.url());
});
Remove or narrow verbose logging when finished, especially if URLs contain sensitive query parameters.
8. Reliability, performance, and cost
- Reliability: use a narrow predicate and register it before the trigger. A URL plus method is often more stable than waiting for a fixed delay. Verify status separately when success matters.
- Parallel waits: if an action legitimately causes multiple independent outcomes, register each wait before the action, then await the promises. Keep each matcher specific so one event cannot satisfy the wrong wait.
- Performance: do not add arbitrary sleeps after the response. If the DOM must update, wait for that DOM condition directly. Inspecting full response bodies inside an asynchronous predicate adds body processing to matching; prefer cheap metadata checks where possible.
- Timeouts: choose a deadline that reflects the operation and test environment. Increasing the timeout may accommodate a slow service, but it cannot correct a wrong URL, missing trigger, or status mismatch.
- Cost: Puppeteer itself does not set a per-response fee in this API. Your browser runtime, compute provider, and target service may have their own costs; no universal price or performance figure follows from the wait method.
9. Or skip the browser setup
If your goal is a screenshot of a page after its network activity settles, ScreenshotNeo can capture a URL with one API request. It is a website screenshot API and MCP server for developers from ScreenshotNeo. The API documentation covers its request options.
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 import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the screenshot; each step can be turned off.
- Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses indicate the page verdict and billing status in headers.
- An MCP server gives AI agents, including Claude and Cursor, the
take_screenshot,get_page_info, andcapture_pdftools. - The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
10. FAQ
Does waitForResponse() wait for the response body too?
It resolves with the matching response object. Read the body afterward with a method such as json(), text(), or buffer().
Can I wait for a response that returns 404?
Yes. Match the URL without requiring a successful status, then inspect status() and handle the result.
Should I wait for network idle instead?
Use a specific response wait when a particular request is the condition. Network-idle navigation settings describe broader network activity and do not identify which response your action needed.
Can one page have more than one response wait?
Yes. Register each wait before the action and make their predicates distinct enough to identify the intended responses.


