Why can Playwright retry an action's readiness checks without paying a round trip per attempt?
answer
- One command, not one poll each
- The loop runs browser-side
- Re-resolved every attempt
- Retries are cheap because nothing crosses
- Timeout reports the last observed state
basics
~20 sBecause the retrying happens on the browser side. One command crosses the already-open connection, and Playwright's browser-side code re-resolves the element and re-checks readiness in a loop until the action succeeds or that command's timeout expires.
solid answer
~50 sAn action is **one** protocol command, not a poll loop over the wire. Your test sends `click` once; element resolution and the readiness gate then run inside the browser, repeatedly, for the lifetime of that single command. A hundred attempts cost a hundred cheap in-browser checks rather than a hundred round trips, which is why auto-waiting can be the default instead of an opt-in: under this driving model it is close to free. A model that has to send a fresh request per poll makes the same loop expensive, and waiting migrates into the test's own code. Two consequences you feel daily: sleeps stop being the tool of choice, and a failure arrives as a timeout error naming what the browser was still waiting for, which is far better evidence than a stale-element failure at an arbitrary moment.
code
typescript · 11 linesimport { test, expect } from '@playwright/test';
test('recalculating a motor quote updates the premium', async ({ page }) => {
await page.goto('https://quotes.example.com/motor/1042');
// One command leaves the test process. Resolution and the readiness gate
// both run browser-side until the action succeeds or the budget ends.
await page.getByRole('button', { name: 'Recalculate premium' }).click({ timeout: 5_000 });
await expect(page.getByTestId('premium')).toHaveText('GBP 412.00');
});go deeper
Recall that you send one instruction and Playwright keeps trying on the browser side until the element is ready or the time runs out. That is why a bare click usually works without a sleep in front of it.
Explain the mechanics: one command, browser-side re-resolution and re-checking per attempt, no traffic between attempts, and a timeout that reports the last state it observed.
Show judgment about what the gate cannot see. Diagnose whether the element was never found, never ready, or racing a re-render, and treat a raised timeout as a hidden defect rather than a fix.
Own the budget policy across a suite: where per-action and test timeouts sit, how slow-but-passing actions get surfaced, and how the team keeps cheap waiting from masking real latency regressions.
Auto-waiting is usually taught as an API feature. It is really a consequence of **where the loop runs**: outside the page but inside the browser, under one command that the test process sent once. ## One command, many attempts When you call an action, the test process sends a single command describing what to do and to which element. Everything after that happens on the browser side: 1. The element description is resolved against the current DOM. 2. Playwright's readiness checks run against the resolved element. 3. If a check does not hold, the browser side waits briefly and starts again from step 1 - re-resolving, not reusing a stale reference. 4. When every check holds, the action is performed and a result is sent back. If the budget runs out first, a timeout error is sent back instead, carrying the state the checks were still waiting on. The wire sees two messages for that whole sequence: the command and its answer. ## Why the model makes this cheap - The connection is **already open**, so the command costs a message, not a handshake. - The polling happens where the DOM already is, so an attempt costs a local query rather than a network exchange. - Because re-resolution happens per attempt, a re-rendered element is simply found again; the loop is not holding a reference that can go stale. - Because the whole loop lives under one command, the timeout is one budget with one meaning, and the error can describe the last observed state. ## Compared with polling from the test process | | Gate on the browser side | Poll from the test process | |---|---|---| | Messages per attempt | none after the initial command | one command plus one result | | Cost of a tighter poll interval | negligible | grows with every attempt | | Where the failure is described | in the browser, which saw every attempt | in the test, which saw only the last answer | | Natural default | wait, because it is nearly free | ask once, and let the author add waiting | That table is the reason two suites with the same intent read so differently: when waiting is cheap it becomes invisible infrastructure, and when it is expensive it becomes the author's problem. ## What this does not fix - **It is not a correctness proof.** The gate says an element looks ready to be acted on; it cannot know your application finished a background write. Choosing a signal that really means "ready" is still a test-design decision. - **It can hide slowness.** An action that quietly retried for nine seconds and then passed leaves no failure behind, only a slow suite. Watch durations, not just pass rates. - **Budgets still need thought.** In Playwright 1.63 a test gets 30 seconds by default and `expect` assertions poll for 5 seconds; the per-action budget is unbounded unless you set `actionTimeout`, so an action can consume the whole test budget and be reported as a test timeout instead of an action timeout. - **A hung browser is invisible to the loop.** If the far end stops responding, no amount of browser-side retrying helps - you get a timeout, and the interesting evidence is the connection, not the element. ## Reading a timeout like a diagnostician A Playwright action timeout is a small report from the browser side. Read it in this order: - Was the element **never resolved**? The description is wrong, or the page never rendered it - a locator or an application problem. - Was it resolved but **never ready**? Something overlays it, animates it, or keeps it disabled - usually a real user-visible defect. - Did it flip between states? A re-render loop or a late data fetch is racing your click. Each of those is a different bug, and you get to tell them apart precisely because every attempt happened next to the DOM instead of one hop away from it. ## The habit this should change Reach for a fixed sleep and you have opted out of a loop that was already running for free, while adding a fixed cost to every run. Reach for a bigger timeout and you have widened a budget without learning why the gate never passed. Under this driving model the honest moves are to fix the description, fix the application state, or assert on the signal that actually means ready.
- If retrying is that cheap, why does Playwright still fail an action instead of waiting forever?Because a budget is what turns a hang into evidence. An unbounded wait would report a broken application as a stalled suite with no diagnosis. The timeout ends the loop and returns the last observed state, so the failure names what never became true. In Playwright 1.63 the test timeout bounds the whole test at 30 seconds by default even when a single action has no timeout of its own.
- Where does this model still leave a race that the browser-side loop cannot see?Anywhere the readable signal is a proxy for readiness. A button can be visible, stable and enabled while a background write is still in flight, so the click lands on a screen that has not caught up. The loop only checks what it can observe in the page; if the true signal is server-side, assert on something that reflects it rather than trusting the gate.
saying these in an interview costs you the question
- Thinks the test process re-sends the command on each poll
- Says auto-waiting removes the need to choose a real signal
- Believes a bigger timeout is a fix for a failing gate
- Adds fixed sleeps to stabilise an already-retrying action
- Reads an action timeout as a slow network rather than a state