Timeouts and retries

Use auto-waiting, assertion retries, and explicit timeouts without hiding real browser failures.

A timeout is not the cause of a failure. It is the moment Playwright PHP stopped waiting for something that never became true.

Good timeout handling starts with the question "what condition did not happen?" Bad timeout handling starts by adding more seconds everywhere. The goal is to wait for product state, keep exceptions local, and use retries only as a signal while you investigate.

Mental model: waits belong to conditions

Playwright PHP already waits in several places:

  • locator actions wait for actionability;
  • browser assertions retry;
  • navigation methods wait for load states;
  • explicit waits can target URLs, responses, selectors, functions, or locators.

Use those waits to describe the condition you need. Do not use time as a substitute for state.

php
use function Playwright\Testing\expect;

$page->getByRole('button', ['name' => 'Save'])->click();

expect($page->getByText('Saved'))->toBeVisible();

This waits for the user-visible result. It does not guess how long saving takes.

Avoid fixed sleeps

Fixed sleeps are usually both too short and too long:

php
// Bad: sometimes too short, always too long when the app is fast.
sleep(2);

Replace the sleep with the state the product promises:

php
expect($page->getByText('Report ready'))->toBeVisible();

If the page depends on a specific backend call, wait for the response and still assert the visible result:

php
$response = $page->waitForResponse('**/api/reports/export', ['timeout' => 15000]);

if (202 !== $response->status()) {
    throw new RuntimeException('Export was not accepted.');
}

expect($page->getByText('Export started'))->toBeVisible();

The response proves the backend answer. The visible assertion proves the user saw the expected state.

Choose the right timeout level

There are three common levels:

php
$page->setDefaultTimeout(5000);
$page->setDefaultNavigationTimeout(15000);

$page->goto('https://app.example.test/reports', ['timeout' => 20000]);

Use them differently:

Level Use for Risk
operation timeout one known slow operation easy to understand
page default timeout a scenario with a different budget can surprise readers if changed mid-test
navigation timeout routes, redirects, reloads should not hide missing content

Keep default changes near setup. Use local timeouts for exceptions.

The number is a budget, not a fix. A short budget gives fast feedback when a normal control is missing. A longer budget is justified when the product operation itself is slow: report generation, payment authorization, import processing, or a queued job.

Use assertion timeouts for slow outcomes

When the product legitimately takes longer, make the budget visible on the assertion:

php
expect($page->getByText('Export complete'))
    ->withTimeout(30000)
    ->toBeVisible();

This tells the next reader that export completion is expected to be slow. It also keeps the rest of the test suite from waiting 30 seconds for normal UI.

For locator-specific waits, prefer assertions when you need to prove a result, and use Locator::waitFor() when you only need a state transition:

php
$page->getByText('Loading')->waitFor(['state' => 'hidden', 'timeout' => 10000]);

expect($page->getByRole('heading', ['name' => 'Results']))->toBeVisible();

The wait handles the transitional element. The assertion proves the final screen.

Classify the timeout before fixing it

Different timeout symptoms point to different fixes:

Symptom Likely cause Better next step
element never appears wrong page, missing data, failed request screenshot, trace, network log
element exists but cannot be clicked overlay, disabled state, animation, frame inspect actionability and page state
URL never changes navigation did not start, validation blocked submit assert validation or wait for URL
assertion sees old text app state did not update inspect response and visible state
response never arrives request was never sent or route pattern wrong log requests before routing

The fix should match the symptom. More time only helps when the condition is correct and genuinely slow.

Use retries as a diagnostic signal

Test-level retries can be useful in CI because they answer one question: "does this failure reproduce every time?" They do not make a flaky test reliable.

Use retries temporarily while you collect evidence:

  • first failure: save trace, screenshot, logs, and failed request evidence;
  • retry passes: suspect race, state leak, data dependency, or timing;
  • retry fails the same way: suspect product bug, fixture issue, route mismatch, or environment drift.

Do not keep permanent retries around known flakes without tracking the root cause. A retry that hides a failure also hides the signal you need to fix it.

Debug a timeout with artifacts

When a timeout fails, collect evidence before changing waits:

php
echo $page->url().PHP_EOL;
$page->screenshot(__DIR__.'/var/playwright-artifacts/timeout.png');

If you use PlaywrightTestCaseTrait, setting PW_TRACE=1 records a trace and writes it on failure. For standalone scripts, start and stop tracing explicitly on the context.

Use the trace to answer:

  • did the action run?
  • did the page navigate?
  • did the element exist?
  • was it hidden, disabled, covered, or inside a frame?
  • did a request fail before the assertion?

If the trace shows the page was already correct before the timeout, inspect the assertion. The locator may target the wrong frame, match several elements, or assert text that differs from what users actually see.

Common mistakes

Raising global timeouts first. This slows every failure and hides the one condition that was wrong.

Retrying the whole test instead of the condition. Browser assertions already retry the condition. Retrying the whole test changes setup, data, and timing.

Waiting for load state after every action. Most user actions need a visible assertion, not a document lifecycle wait.

Treating a timeout as a selector problem only. Selectors are one cause. Missing data, auth state, failed requests, and overlays are just as common.

Verification checklist

  • No fixed sleeps for UI readiness.
  • Assertions describe product state.
  • Longer timeouts are local and justified.
  • Navigation timeouts are not used to wait for app content.
  • Retries are temporary or explicitly tracked.
  • Timeout failures produce screenshot or trace evidence.

Go next