How to Wait for a Network Request in Playwright
Learn how to wait for requests, responses, and completed downloads in Playwright with reliable predicates, timeouts, debugging, and examples.
Start the wait before the action that causes the traffic. Create a promise with page.waitForRequest() or page.waitForResponse(), trigger the click or form submission, then await the promise. Match the intended request with an exact URL, glob, regular expression, or predicate.
import { test, expect } from '@playwright/test';
test('submits an order', async ({ page }) => {
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/orders') &&
response.request().method() === 'POST'
);
await page.getByRole('button', { name: 'Submit order' }).click();
const response = await responsePromise;
expect(response.status()).toBe(201);
await expect(page.getByText('Order confirmed')).toBeVisible();
});
Playwright documents this ordering in its Network guide and Page API. Waiting first prevents a fast request from completing before the test starts listening.
Choose the event you actually need
| Need | Use | What you receive |
|---|---|---|
| Confirm the browser issued traffic | page.waitForRequest() |
A Request, including URL, method, headers and post data |
| Check status, headers or response metadata | page.waitForResponse() |
A Response associated with the request |
| Know that the body finished downloading | requestfinished or a response followed by body reads |
The completed request lifecycle event |
| Observe many events for diagnostics | page.on('request'), page.on('response'), page.on('requestfailed') |
Event callbacks for ongoing logging |
A response with status 404 or 503 is still an HTTP response. A transport error, DNS failure or aborted connection can emit requestfailed without a response. Assert the status yourself when success matters.
Wait for an outgoing request
Use waitForRequest when the assertion concerns what the browser sent.
const requestPromise = page.waitForRequest(request =>
request.url().includes('/api/search') &&
request.method() === 'GET'
);
await page.getByRole('button', { name: 'Search' }).click();
const request = await requestPromise;
console.log(request.url());
console.log(request.method());
console.log(request.postData());
Check query parameters and request data
const requestPromise = page.waitForRequest(request => {
if (request.url() !== 'https://example.test/api/search?q=playwright') return false;
if (request.method() !== 'GET') return false;
return request.headerValue('accept')?.includes('application/json') ?? false;
});
await page.getByRole('button', { name: 'Search' }).click();
const request = await requestPromise;
For POST requests, parse JSON only when a body exists:
const requestPromise = page.waitForRequest(request =>
request.url().endsWith('/api/orders') && request.method() === 'POST'
);
await page.getByRole('button', { name: 'Submit order' }).click();
const request = await requestPromise;
const payload = request.postDataJSON();
expect(payload.items).toHaveLength(1);
Wait for an API response
Use waitForResponse when you need the HTTP status, headers or response body.
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/profile') &&
response.request().method() === 'PATCH'
);
await page.getByRole('button', { name: 'Save profile' }).click();
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.displayName).toBe('Ada');
Combine URL, method and status to avoid matching an unrelated call:
const responsePromise = page.waitForResponse(response =>
response.url().match(/\/api\/users\/\d+$/) !== null &&
response.request().method() === 'GET' &&
response.status() === 200
);
await page.getByRole('link', { name: 'Account' }).click();
const response = await responsePromise;
Match URLs safely
Exact URL
const responsePromise = page.waitForResponse('https://example.test/api/cart');
Exact strings are clearest when the URL never changes.
Glob patterns
const responsePromise = page.waitForResponse('**/api/orders/*');
const assetPromise = page.waitForResponse('**/*.{png,jpg}');
Playwright glob patterns use * for characters other than /, ** for characters that can include slashes, ? as a literal question mark, and brace lists such as {png,jpg}. Use ** when paths may contain nested segments.
Regular expressions
const responsePromise = page.waitForResponse(/\/api\/orders\/\d+$/);
Predicates
Predicates are the most flexible option because they can inspect the request attached to a response:
const responsePromise = page.waitForResponse(response => {
const request = response.request();
return response.url().includes('/api/export') &&
request.method() === 'POST' &&
response.headers()['content-type']?.includes('application/json');
});
Use the correct ordering
This is incorrect when the response depends on the click:
// Do not do this: the click cannot run until the wait resolves.
await page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: 'Submit order' }).click();
Create the promise without awaiting it, perform the action, and then await the saved promise:
const responsePromise = page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: 'Submit order' }).click();
const response = await responsePromise;
The same pattern works for links, keyboard actions, file uploads and any other action that initiates the request.
Wait for a request and the resulting UI
Network completion does not always mean the interface has rendered the result. Pair the network wait with a user-visible assertion.
const responsePromise = page.waitForResponse(response =>
response.url().endsWith('/api/login') && response.request().method() === 'POST'
);
await page.getByLabel('Email').fill('ada@example.test');
await page.getByLabel('Password').fill('correct-horse-battery-staple');
await page.getByRole('button', { name: 'Sign in' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
Request lifecycle and failures
For a normal request, Playwright emits request when it is issued, response when status and headers arrive, and requestfinished after the response body downloads. Redirects finish the original request and create a new request for the redirected URL.
If the browser cannot complete the transport, Playwright emits requestfailed instead of requestfinished. An HTTP error such as 404 is different: it still produces a response and can finish normally.
Timeouts and configuration
Timeout behavior is version-sensitive, so check the API reference for the Playwright version installed in your project. Configure a timeout per wait when a particular operation is expected to take longer:
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/report'),
{ timeout: 60_000 }
);
You can also configure page or context defaults in your Playwright setup. Keep waits bounded: an unbounded wait can hide a broken trigger and make a test hang.
Python Playwright example
from playwright.sync_api import sync_playwright, expect
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.test/orders")
with page.expect_response(
lambda response: "/api/orders" in response.url
and response.request.method == "POST"
and response.status == 201
) as response_info:
page.get_by_role("button", name="Submit order").click()
response = response_info.value
assert response.ok
expect(page.get_by_text("Order confirmed")).to_be_visible()
browser.close()
Node.js example without the test runner
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.test/orders');
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/orders') &&
response.request().method() === 'POST'
);
await page.getByRole('button', { name: 'Submit order' }).click();
const response = await responsePromise;
if (response.status() !== 201) {
throw new Error(`Unexpected status: ${response.status()}`);
}
await browser.close();
cURL for checking the endpoint separately
cURL does not wait for browser-generated traffic, but it is useful for confirming that an API endpoint, method and response headers behave as expected outside the browser.
curl --include --request POST \
--header 'Content-Type: application/json' \
--data '{"items":[{"sku":"demo","quantity":1}]}' \
https://example.test/api/orders
Observe traffic for diagnostics
page.on('request', request => {
console.log('request', request.method(), request.url());
});
page.on('response', response => {
console.log('response', response.status(), response.url());
});
page.on('requestfailed', request => {
console.log('failed', request.method(), request.url(), request.failure());
});
Remove broad logging after diagnosis or limit it to the host and route under test to keep CI output useful.
Why a wait times out
| Symptom | Likely cause | Fix |
|---|---|---|
| Timeout after clicking | The click did not trigger the expected route | Log requests, verify the URL and confirm the control is enabled |
| Wrong response matched | The predicate is too broad | Add the HTTP method, exact path, query condition or status |
| Wait starts too late | The promise was awaited before the action | Create the promise first, then trigger the action |
| Expected success but received 404 or 503 | HTTP errors still produce responses | Assert response.status() or response.ok() |
| No response event appears | Transport failure, aborted request or service-worker handling | Listen for requestfailed; inspect service workers and routing |
| Multiple requests share a path | Prefetching, retries or analytics traffic | Match method, query, request payload and response status |
| Body is unavailable or incomplete | The test needs the finished lifecycle | Use requestfinished or await the response body operation |
Service workers and routing
Service workers can intercept traffic before built-in routing or interception sees it. If page.route() or browserContext.route() appears to miss a request, the Playwright Network guide recommends blocking service workers for that routing scenario:
const context = await browser.newContext({ serviceWorkers: 'block' });
const page = await context.newPage();
Apply this targeted change only when service-worker interception explains the missing event; ordinary response waits do not require it.
Why networkidle is usually the wrong wait
Playwright defines networkidle as having no network connections for at least 500 ms and marks it discouraged for testing. Applications may keep analytics, polling or streaming connections open, and an idle network does not prove that the required UI state is ready. Wait for the specific response and then assert the visible result instead.
Performance, reliability and cost
- Performance: narrow predicates resolve on the intended event and avoid waiting for unrelated assets. Do not parse large response bodies unless the assertion needs them.
- Reliability: assert method, route and status; pair protocol checks with a UI assertion; keep timeouts finite and appropriate to the endpoint.
- Retries: distinguish application retries from duplicate user actions. A broad URL predicate may match the first failed attempt instead of the successful retry.
- Parallel tests: isolate browser contexts and test data so another test cannot produce a matching request.
- Cost: Playwright itself is open-source test tooling; your practical cost comes from browser runtime, CI minutes and any API or hosted browser service used by the test.
Or skip the browser setup
If your goal is a clean image of a page after it loads rather than an assertion about a particular API event, ScreenshotNeo provides a single screenshot request. Its capture flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and the response reports the page verdict and billing result in X-Page-Verdict and X-Billed headers.
See the ScreenshotNeo API documentation for all 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}`);
ScreenshotNeo also offers an MCP server so Claude, Cursor and other MCP clients can take screenshots, inspect pages and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Checklist
- Identify whether you need the request, response or completed download.
- Create the wait promise before the triggering action.
- Match URL and HTTP method narrowly.
- Assert status separately from event completion.
- Assert the resulting UI state when the user experience matters.
- Log request, response and failure events when diagnosing a timeout.
- Investigate service workers when routing or interception misses traffic.
- Prefer a specific response wait over
networkidle.
FAQ
Should I use waitForRequest or waitForResponse?
Use waitForRequest to inspect what the browser sent. Use waitForResponse to inspect status, headers or response data.
Can I wait for a request after clicking?
Register the promise before clicking. Awaiting the wait first prevents the click from running.
Does a 500 response trigger requestfailed?
No. An HTTP 500 is a response. requestfailed is for transport-level failures; check the response status for HTTP errors.
When should I use requestfinished?
Use it when completion of the response body download is the event your test needs, rather than merely receipt of status and headers.
Is networkidle reliable for waiting on an API?
It is a poor substitute for a specific API wait. Match the response you need and assert the resulting page state.


