The test fails after half a minute of nothing:
Test timeout of 30000ms exceeded.
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
- waiting for getByRole('button', { name: 'Save' })
The obvious reaction is to raise the timeout. That is almost always wrong, and it usually turns a 30 second failure into a 60 second failure.
The error is more helpful than it looks. It tells you exactly what Playwright was waiting for. The fix is to read that, not to give it more time.
Playwright has three timeouts, and they are not the same
Most confusion here comes from treating "timeout" as one thing. It is three, with different defaults.
| Timeout | Default | What it bounds |
|---|---|---|
| Test timeout | 30 seconds | The whole test, start to finish |
| Expect timeout | 5 seconds | One auto-retrying assertion |
| Action timeout | No timeout | One action, like a click or fill |
Two things surprise people here.
Actions have no timeout of their own by default. A click does not fail after some action limit, because there is not one. It keeps waiting until the whole test runs out. That is why a stuck click reports "Test timeout of 30000ms exceeded" rather than an action error.
Assertions get their own, much shorter clock. An expect(...).toBeVisible() gives up after 5 seconds, not 30.
So the number in your error already tells you which one fired. 30000ms is the test. 5000ms is an assertion.
You can configure each one:
// playwright.config.ts
export default defineConfig({
timeout: 30_000, // per test
expect: { timeout: 5_000 }, // per auto-retrying assertion
use: { actionTimeout: 10_000 }, // per action, off by default
});
Read the call log first
This is the part people scroll past, and it is the answer:
Call log:
- waiting for getByRole('button', { name: 'Save' })
Playwright is telling you the exact locator it sat waiting on. Before changing any config, ask one question: why would that locator never resolve?
A richer call log tells you even more:
Call log:
- waiting for getByRole('button', { name: 'Save' })
- locator resolved to <button disabled>Save</button>
- element is not enabled
- retrying click action
That is not a timing problem at all. The element was found immediately. It is disabled, and it stayed disabled. No extra seconds will help. Something earlier in the flow failed to enable it, and that is the real bug.
What Playwright waits for before it acts
Playwright does not click the moment it finds an element. Before an action it checks the element is:
- attached to the DOM
- visible
- stable, meaning it has stopped moving
- enabled
- able to receive events, meaning nothing covers it
It retries these checks until they all pass. So a timeout means at least one of those never became true. The call log usually names which.
This is worth knowing because it reframes the error. It is not "Playwright was too slow". It is "this element never became usable, and here is the check that failed".
The five real causes
1. The element never appears
The selector is wrong, or the element genuinely never renders. Confirm the selector against the real page before assuming timing.
npx playwright test --debug
The debugger lets you step and try locators live, which settles this in seconds.
2. The element exists but stays disabled or covered
The call log says "element is not enabled" or "intercepts pointer events". A cookie banner, a loading overlay, or a form that never validated. Fix the cause, not the clock. This is the same family as the strict mode and locator problems you meet elsewhere.
3. The locator matches something invisible
Responsive layouts often render a mobile and a desktop copy of the same control, hiding one. If your locator matched the hidden copy, it will never become visible.
// Ask for the one a user can actually see.
page.getByRole('button', { name: 'Save' }).filter({ visible: true });
4. The page never finished what it started
A failed network request, an error swallowed in the console, or a spinner that never resolves. The application is broken, and your test is correctly reporting it. This is a real bug, not a test problem.
5. The whole test is simply long
Many steps, each legitimately slow. Here, and only here, raising the test timeout is the correct fix.
test('long checkout journey', async ({ page }) => {
test.setTimeout(60_000); // this specific test genuinely needs longer
});
Raise it for the one test that needs it. Raising the global default hides real failures across the whole suite.
Why raising the timeout is usually the wrong first move
If an element never becomes usable, waiting longer changes nothing except how long you wait to find out. You turn a fast failure into a slow one, and slow failures make a suite people stop trusting.
There is a second cost. A global timeout raise masks genuine regressions. A page that used to respond in two seconds and now takes twenty is a real problem, and a 60 second timeout hides it until a customer complains.
Raise a timeout when the work is genuinely slower. Fix the cause when the element is not usable. The call log tells you which situation you are in.
Do not fix it with waitForTimeout
await page.waitForTimeout(5000); // please do not
The Playwright documentation is direct about this: it exists for debugging and should not be used in production tests. It is wrong in both directions, wasting time when the page is ready and still failing when it is slow.
Use a web-first assertion instead. It retries until the condition is true, then continues at once:
await expect(page.getByTestId('toast')).toBeVisible();
That waits for the real condition, so it is both faster and more reliable than any fixed number.
Use the trace, it removes the guessing
This is Playwright's real advantage over reading logs.
// playwright.config.ts
use: { trace: 'on-first-retry' },
Then open the trace from the failed run:
npx playwright show-trace trace.zip
You get the DOM at the moment of failure, every action with timings, the network, and the console. Instead of guessing why an element never became visible, you look at the page as it was and see the overlay sitting on top of it.
For a failure you did not watch happen, especially in CI, this is the fastest route to the cause.
Remember this
Read the number to know which timeout fired, then read the call log to see what it waited for. Actions have no timeout of their own, so a stuck action reports the test timeout. Most timeouts are not slowness, they are an element that never became visible, enabled or uncovered. Raise a timeout only when the work is really longer, and use the trace instead of guessing.
The Playwright track goes deeper on where auto-waiting stops helping you: the limits of auto-waiting.
Related: how to fix flaky tests and waiting compared across all three tools.
Reference: Playwright timeouts.