Playwright Timeout of 30000ms Exceeded: Fix It Fast
Read the trace to find which phase burned the 30s.
20+ years shipping production backend systems. Everything here is grounded in real deployments.
- ✓A Playwright Test project runnable with npx playwright test
- ✓Basic JavaScript/TypeScript and async/await comfort
- ✓A failing or slow test you can open in the trace viewer
- Playwright gives each test 30 seconds by default (test body plus fixtures and hooks) — expiry means the budget burned, not that 30s is wrong
- Open the trace timeline first: locator-resolution spin means a bad selector, network-dominated time means a slow app or backend
- Fix the phase, not the budget: correct locators, seed data via API, use web-first assertions with explicit timeouts
- Adjust the narrowest layer (test.setTimeout, expect timeout, actionTimeout) and only with measured cause
Think of the 30-second timeout as a taxi meter with a fixed fare limit. If the ride fails, the cause matters: stuck in real traffic (slow app), the driver going to the wrong address (bad selector), waiting for a passenger who is late (late state), or booking five stops as one trip (bloated test). Raising the fare limit only helps real traffic — the other three need a new route, not more money.
The error is admirably blunt: Timeout of 30000ms exceeded. Thirty seconds, gone, and Playwright hands you a stack pointing at a line that looks innocent — a click, a fill, an assertion you have watched pass a hundred times. Your first impulse is to raise the timeout and move on. Resist it for ten minutes, because that number is a symptom with at least four different diseases behind it.
A test can burn thirty seconds because the app is genuinely slow, because the selector never matches anything, because the assertion waits on state that arrives late, or because the test does the work of five tests in one body. Raising the timeout cures exactly one of those (and only when the slowness is legitimate). The other three keep failing at 60 seconds exactly as they failed at 30 — now twice as expensive.
This article teaches the timeout layers — test, expect, action, navigation — and the discipline of spending from them deliberately. You will learn to read a trace like a budget statement, to separate slow selectors from slow apps in seconds, and to fix the phase that actually burns the budget. Thirty seconds is generous for one behavior; it is only tight when the test wastes it.
What Timeout of 30000ms Exceeded Means
Playwright Test enforces a per-test timeout of 30 seconds by default, and that budget covers more than the test body: fixture setup, beforeEach hooks, and the test function all draw from the same 30 seconds. (Teardown and afterEach hooks get a separate budget of equal size after the test finishes.) When the budget expires, Playwright aborts with 'Timeout of 30000ms exceeded' and points at whatever line was executing — which is where the time ran out, not necessarily where the problem lives.
That distinction matters because the blamed line is usually innocent. A click that 'times out' may have waited behind a ten-second fixture and a fifteen-second navigation, inheriting a nearly empty budget. Reading only the error line leads to 'fixing' a healthy click while the real overspend hides in setup. Always account for the full budget: what ran before the blamed line, and how long did each phase take?
Treat the error as an accounting question from the start. The test spent thirty seconds somewhere — your job is the ledger, not the verdict. The trace viewer is that ledger, itemizing every action, assertion, and wait with durations. Open it before forming any hypothesis, because timeout causes that feel obvious from the error line are wrong often enough to make guessing embarrassing.
Slow Selector or Slow App: Finding the Real Culprit
Two failures wear the same error message and demand opposite fixes. A slow selector means the locator never resolves to an actionable element: zero matches, many matches, or matches on hidden nodes the action refuses to touch. The trace shows a single action spinning on resolution while the page sits idle — the app is ready and waiting, but the test cannot point at anything. The fix is always in the test: correct the locator, scope it, prefer roles and test ids.
A slow app means every locator resolves promptly while the budget burns on real work: API responses crawling, heavy renders blocking, third-party scripts stalling navigation. The trace shows snappy resolution with long network bars and late paint completion. The fix lives outside the test: seed smaller data, stub the slow dependency, fix the endpoint — and only then consider budgeting more time for genuinely heavy flows.
Distinguish them in under a minute. Open the trace timeline and ask one question: did the failing step spend its time resolving the target or waiting on the world? Resolution-spin is a selector bug (fix the test now). World-waiting is an app or data problem (fix the cause, budget the remainder). Confuse the two and you will either gold-plate locators for a backend outage or raise timeouts for a typo — both expensive, both avoidable.
Timeout Layers: Test, Expect, Action, Navigation
Playwright's timeouts form layers, and each layer answers a different question. The test timeout (default 30s, config timeout or test.setTimeout(120_000)) caps the whole test including setup — widen it when one behavior legitimately needs longer. The expect timeout (default 5s, config expect.timeout or per-assertion { timeout: 10_000 }) caps assertion polling — widen it when the state arrives slowly but surely. Action and navigation timeouts (default unlimited, config use.actionTimeout / use.navigationTimeout or per-call options) cap individual steps — set them to fail fast on hung interactions rather than burning the whole test budget.
deserves special mention: it triples the test's timeout, expressing 'this path is known-heavy' (migrations, uploads, cold starts) without hard-coding milliseconds that rot when defaults change. Prefer it over magic numbers for legitimate heaviness, and reserve test.slow()test.setTimeout() for precise budgets with a comment naming the cause.
The discipline is narrowest-layer-first. A slow assertion gets an assertion timeout, not a test timeout; a hung navigation gets a navigation cap, not a global bump. Each layer widened is failure signal lost at that granularity — a test with a 120-second budget that hangs teaches you nothing for two minutes. Spend budget where the trace proves the need, document the reason beside the number, and review timeout changes like the load-bearing decisions they are.
Fix the App and the Locator Before Raising Timeouts
Before budgeting more time, shrink the work. Seed test data through APIs or storage state instead of clicking through setup flows — UI-driven setup routinely consumes half the 30 seconds before the actual behavior starts. Stub slow third parties with page.route so the test times your app, not someone else's analytics endpoint. Each deterministic input removes variance the timeout would otherwise have to cover.
Then synchronize with polling, not sleeping. Web-first assertions like toHaveText and toBeVisible re-check until the condition holds, passing the instant the app is ready. They compress fast days to milliseconds while remaining patient on slow ones — the exact property fixed sleeps lack. Reserve waitForTimeout for genuinely time-based behavior (auto-dismissing toasts), never for data arrival.
Only after shrinking and synchronizing does budgeting enter. Give the proven-slow step its explicit timeout with a comment citing the measured cause ('aggregation endpoint p95 ~12s'), keep everything else at defaults, and watch the test's total duration settle far below the ceiling. A test shaped this way has headroom by construction: normal variance fits comfortably, and only a genuine regression reaches the budget — which is precisely when you want the alarm.
Trace Viewer: Replaying the Failed 30 Seconds
The trace viewer replays the failed thirty seconds as an interactive film: a timeline of every action with durations, a film strip of screenshots per step, DOM snapshots you can inspect with DevTools, plus network, console, and error tabs per action. For timeouts it answers the only question that matters — where did the budget go — with evidence instead of theories. The bar that spans twenty seconds is the culprit; everything else is commentary.
Work the viewer in order. Timeline first: identify the budget-burning step and whether it burned on resolution, network, or assertion polling. Film strip second: see what the user would have seen — stuck spinner, blank page, half-rendered list — which distinguishes app-stuck from test-confused. Network and console third: confirm slow or failed requests and catch the app errors the test never surfaced. Most timeouts resolve by the end of this pass with the fix obvious.
Make traces always available where they matter. Keep trace: 'on-first-retry' in config so CI failures arrive with replays attached, and force --trace on locally when hunting a timeout. A timeout report without a trace is a support ticket missing its logs — technically complete, practically useless. The viewer turns the bluntest error in Playwright into the most debuggable one.
Config That Keeps Suites Fast Without Flakes
Durable timeout health comes from policy, not heroics. Budget from measured p95 step durations, not vibes: steps whose p95 approaches their layer's default earn explicit, commented timeouts, while everything else stays at defaults so new slowdowns trip alarms early. Review every timeout increase like a capacity change — with trace evidence attached — because each one trades failure signal for patience.
Structure tests to make the policy easy. One behavior per test with shared authenticated fixtures keeps every test far from the ceiling with headroom to spare; journey tests that need length get explicit with an owner and a reason. Retries stay reserved for genuine intermittency, never as a substitute for duration hygiene — a test that needs 45 seconds must be budgeted or slimmed, not retried.test.slow()
Finally, watch the trend line. Plot suite and spec durations over releases and treat upward drift as a regression: the page that creeps from 5 to 25 seconds across quarters is telling you about a growing payload, a missing index, or an accumulating third-party tax. Timeouts configured as alarms catch that story early; timeouts configured as wallpaper hide it until users complain. Spend budget deliberately, and the suite stays both fast and honest.
The Team That Doubled Every Timeout and Still Timed Out
test.slow() with an owner.- A timeout budget spent without trace evidence is a guess with a config file. Require the timeline screenshot in every timeout-related review.
- Global timeout increases are contagious — one bump teaches the team that budgets are negotiable instead of diagnostic.
- Separate duration problems (fix the phase) from intermittency problems (then consider retries). Mixing them wastes both fixes.
npx playwright test spec.js --trace on), open the trace, and read the timeline: find the single action or assertion consuming the bulk of the 30s. If it is locator resolution spinning, fix the selector (step 2). If it is network or render, fix the app/data (step 3). Never change config before this step.page.locator(...).count() probe. Zero matches means a wrong selector; multiple matches means strict-mode-adjacent ambiguity; matches on hidden nodes means the action waits for visibility that never comes. Switch to getByRole/getByTestId and re-run.expect() burns the remaining timewaitForTimeout with await expect(locator).toHaveText(...) (or toBeVisible) carrying an explicit { timeout } where justified. The assertion polls and passes the instant the state arrives instead of charging the full delay on every run.test.slow() or test.setTimeout(). Each test should finish with headroom, not brush the ceiling.| File | Command / Code | Purpose |
|---|---|---|
| playwright.config.js | export default defineConfig({ | What Timeout of 30000ms Exceeded Means |
| tests | await page.locator('.btn-primary').click() // 47 matches? hidden? detached? | Slow Selector or Slow App |
| tests | test('report totals', async ({ page }) => { | Fix the App and the Locator Before Raising Timeouts |
Key takeaways
Common mistakes to avoid
5 patternsRaising the global timeout instead of diagnosing
timeout only after profiling proves the step needs it, and comment the reason. Global bumps hide real slowdowns — a page that drifts from 5 to 25 seconds deserves an investigation, not a config change.Using waitForTimeout as a synchronization tool
await expect(locator).toHaveText('Saved')) that poll until the condition holds. Reserve waitForTimeout for true time-based behavior like auto-dismissing toasts.Load-testing the backend inside a UI timeout
Stuffing an entire user journey into one test
Treating every timeout as 'the app is slow'
{ timeout: 10000 }) on the slow step. Let the trace tell you which step is slow before you budget for it.Interview Questions on This Topic
What does 'Timeout of 30000ms exceeded' mean in Playwright?
Frequently Asked Questions
20+ years shipping production backend systems. Everything here is grounded in real deployments.
That's Playwright. Mark it forged?
5 min read · try the examples if you haven't