How to Wait for API Responses in Playwright — The Complete Guide to expect_response() in Python

“The next assertion runs before the API call triggered by the button click has finished” — this is one of the most common problems in Playwright tests that deal with asynchronous behavior. In Playwright for Python, page.expect_response() waits until a specific API response comes back, and lets you verify its status code and even the contents of its JSON. This article covers the basic syntax, how to specify URLs, how to wait for several requests at once, and the failures that tend to show up in real projects.

The key point is to “declare the wait before you perform the action.” If you reverse the order, the response slips through and you end up with a timeout that is hard to explain.

📌 Who this article is for

  • Playwright (Python) users whose tests are unstable because of API timing after a click
  • Anyone who currently relies on time.sleep() or networkidle and wants to replace it
  • Anyone who wants to verify things the screen never shows, such as the API status code or the request body
  • Anyone reading the Node.js docs who isn’t sure how to write the same thing in Python

✅ What you’ll get from this article

  • The basic form of expect_response() and the correct order to write it in
  • When to use a URL pattern, a regular expression, or a predicate function
  • How to verify the contents of both the response and the request

👤 About the author

Written by Yoshi, a QA and test automation engineer with more than 15 years of hands-on experience. Has worked on stabilizing UI tests that involve asynchronous processing, using Playwright in real projects.

📌 Conclusion

  • In Python, perform the action inside with page.expect_response(...) as info: and read the response from info.value
  • Declare the wait before the action. Writing it after the action can miss the response
  • Naming the API you want to wait for, instead of using time.sleep() or networkidle, makes tests faster and more stable

Tests that wait for an API response tend to run into problems like these:

  • The assertion runs right after the click, and fails on a screen that doesn’t have the data yet
  • A time.sleep(3) gets added “just to be safe,” and the whole suite slows down
  • The screen looks fine, but the API actually returned a 400
  • Node.js samples for waitForResponse don’t work as-is in Python

expect_response() gives you a clean way to sort these out.

Where expect_response() Fits

Auto-waiting handles the wait for “an element to become actionable,” but waiting for an API response is outside its scope. expect_response() fills that gap as an explicit wait. It also differs from its neighbors: page.route() controls (replaces) network traffic, whereas expect_response() waits for traffic and verifies it. And unlike the DevTools Network tab, which only lets you observe traffic, it runs inside the test itself.

💡 Note: In Node.js (JavaScript) the method is called page.waitForResponse(). In Python it is page.expect_response(). For the latest behavior in Python, check the official documentation for the version you use.

① The Basic Form: Declare the Wait First

def test_create_order(page):
    page.goto("/orders/new")

    # ① Declare the wait, and perform the action inside that with block
    with page.expect_response("**/api/orders") as response_info:
        page.get_by_role("button", name="Place Order").click()

    # ② After leaving the with block, take the response and verify it
    response = response_info.value
    assert response.status == 201
    assert response.json()["status"] == "created"

❌ The Order You Should Avoid

# ❌ The wait is declared after the click
page.get_by_role("button", name="Place Order").click()
with page.expect_response("**/api/orders") as response_info:
    pass  # a response that already came back can't be caught, so this times out
⚠️ Caution: The faster the API, the more likely a wait written after the action is to miss the response. It can also look like “it passes locally but only fails in CI.” Always write it so that the action sits inside the with block.

② How to Specify the Response to Wait For

MethodExampleBest suited for
Glob pattern"**/api/orders"A fixed URL you want to specify simply
Regular expressionre.compile(r"/api/orders/\d+")URLs where part of the path, such as an ID, changes every time
Predicate functionlambda r: ...Narrowing by something other than the URL, such as HTTP method or status
import re

# Regular expression: URLs like /api/orders/123
with page.expect_response(re.compile(r"/api/orders/\d+")) as info:
    page.get_by_role("link", name="Order details").click()

# Predicate: only successful responses to a POST
with page.expect_response(
    lambda r: "/api/orders" in r.url
    and r.request.method == "POST"
    and r.ok
) as info:
    page.get_by_role("button", name="Place Order").click()
🔍 A common real-world trap: On screens where both a GET (list) and a POST (create) hit the same URL, specifying only the URL can pick up the wrong one. When in doubt, narrow it down to the HTTP method with a predicate.

③ Verifying the Contents of the Response and Request

with page.expect_response("**/api/orders") as info:
    page.get_by_role("button", name="Place Order").click()

response = info.value

# Verify the response
assert response.status == 201
assert "application/json" in response.headers.get("content-type", "")
body = response.json()
assert body["total"] == 1500

# Verify the request that was sent (including what the screen never shows)
request = response.request
assert request.method == "POST"
assert request.post_data_json["quantity"] == 3
💡 Practical tip: A bug where the screen looks successful but the API actually sent an unexpected request body is not something UI assertions can catch. Being able to inspect what was sent through response.request is a real strength of expect_response().

④ How It Differs from sleep and networkidle

ApproachProblem
time.sleep(3)Too short on a slow environment, wasted time on a fast one. Always slow and still unstable
wait_for_load_state("networkidle")Never settles when there is polling or a persistent connection. The official docs also discourage it for testing
expect_response()Lets you name only the API you care about, and moves on the moment it returns
⚠️ Caution: Waiting for the API response alone does not guarantee that the result has been rendered on screen. If you need to confirm what is displayed, combine it with a web-first assertion such as expect(locator).to_be_visible().

⑤ Waiting for Multiple Responses and Setting a Timeout

# Wait for both of the two APIs that fire when the page loads
with page.expect_response("**/api/user") as user_info, \
     page.expect_response("**/api/orders") as orders_info:
    page.goto("/dashboard")

assert user_info.value.ok
assert orders_info.value.ok

# For a slow API, extend the timeout for that request only (in milliseconds)
with page.expect_response("**/api/report", timeout=60_000) as info:
    page.get_by_role("button", name="Generate Report").click()

Common Failure Patterns in Real Projects

① Writing the wait after the action and hitting a timeout

If the response comes back first, it can’t be caught. Always put the action inside the with block.

② A URL pattern that is too broad picks up a different request

A wide pattern such as "**/api/**" ends the wait on the first unrelated response that matches. Specify a concrete path and, where needed, the HTTP method too.

③ Reading info.value inside the with block

The response is only settled after you leave the with block. Do the extraction and assertions outside it.

④ Adding the wait to every test and slowing the run down

There’s no need to add expect_response() where Auto-waiting is enough. Keep it for the cases where “the next assertion depends on the result of the API.”

FAQ

Q. Is it the same as waitForResponse in Node.js?

The purpose is the same, but the syntax differs. The Node.js version is usually written together with Promise.all, while the Python version uses a with statement as a context manager.

Q. Can it wait for 404 or 500 responses too?

Yes. expect_response() can capture 404 and 500 responses as long as they match your condition. If you only want successful responses, specify r.ok in the predicate.

Q. Can it wait for responses mocked with page.route()?

Even when you mock a response with page.route(), you can still wait for the same traffic with expect_response(). When verifying a mocked response, though, the intent is clearer if the test is designed to check that what route.fulfill() returned is reflected correctly on screen. For how to build the mock itself, see an article on API mocking with page.route().

Q. What if I only want to confirm that a request was sent, not the response?

You can use page.expect_request(). It is written the same way as expect_response(), and you get the request from info.value.

Q. What is the default timeout?

Around 30 seconds is the usual guideline, but it can change with the version and your configuration. For slow APIs, it is a good idea to pass timeout= in milliseconds for that call alone.

📋 Summary

  • In Python, perform the action inside with page.expect_response(...) as info: and get the result from info.value
  • Declare the wait before the action. Reversing the order can miss the response
  • Specify the URL with a glob, a regular expression, or a predicate, and narrowing down to the HTTP method is the safer choice
  • Rather than leaning on sleep or networkidle, name the API you want to wait for for faster, more stable tests

Waiting for API responses is one of the most frequent causes of flaky tests. As a first step, pick one place where you currently use time.sleep() and try replacing it with expect_response().

Copied title and URL