Home › Testing › Playwright Timeout of 30000ms Exceeded: Fix It Fast
Beginner 5 min · September 23, 2026

Playwright Timeout of 30000ms Exceeded: Fix It Fast

Read the trace to find which phase burned the 30s.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Everything here is grounded in real deployments.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 9 min
  • ✓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
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is Playwright Test Timeout of 30000ms Exceeded?

The Playwright Test runner enforces a per-test timeout — 30,000 milliseconds by default — covering the test function together with its fixture setup and beforeEach hooks. When that budget expires, the run aborts with 'Timeout of 30000ms exceeded'. The error reports where time ran out, which is rarely where time was wasted: a click blamed at second 30 may have inherited a budget already drained by slow fixtures and navigation.

★
Think of the 30-second timeout as a taxi meter with a fixed fare limit.

Below the test timeout sit narrower layers. The expect timeout (5 seconds default) bounds assertion polling for web-first assertions like toHaveText. Action and navigation timeouts (unlimited by default) cap individual steps when configured via use() or per-call options. test.setTimeout() rewrites the whole-test budget for one test, while test.slow() triples it for known-heavy paths.

Each layer answers a different question, and the fix belongs at the narrowest layer the evidence implicates.

The trace viewer ties the system together: it records every action, assertion, screenshot, network call, and console message with durations, so a timeout becomes an itemized budget statement. Slow-selector failures show resolution spin with an idle page; slow-app failures show snappy resolution with long network bars.

That single visual distinction routes each timeout to its correct fix — locator correction versus data, backend, or synchronization work — before any timeout number changes.

Plain-English First

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.

playwright.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// What the 30s budget covers (playwright.config.js defaults)
import { defineConfig } from '@playwright/test'

export default defineConfig({
  timeout: 30_000, // per-test budget: fixtures + beforeEach + test body
  expect: {
    timeout: 5_000, // assertion polling budget (toHaveText, toBeVisible...)
  },
  use: {
    actionTimeout: 0,      // per-action cap (0 = no limit, uses test budget)
    navigationTimeout: 0,  // per-navigation cap (0 = no limit)
    trace: 'on-first-retry', // record traces when retries run
  },
})
// Timed-out test prints: "Timeout of 30000ms exceeded."
Try it live
📊 Production Insight
Timeout triage without a trace is guessing with confidence. Make 'show me the timeline' the team's reflex before any timeout change.
🎯 Key Takeaway
The 30s covers fixtures, hooks, and body together — the blamed line is where time ran out, not where it was wasted.

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.

tests/timeout-triage.spec.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Slow selector: spins on resolution (nothing to act on)
// Trace shows: locator '.btn-primary' resolving for 29s...
await page.locator('.btn-primary').click() // 47 matches? hidden? detached?

// Slow app: everything resolves, network/render takes the time
// Trace shows: actions resolve instantly, API + paint consume 25s
await page.getByRole('button', { name: 'Load report' }).click()
await expect(page.getByTestId('report-total')).toHaveText('$42,000')

// Quick probe to tell them apart (run in the trace or a scratch test)
await expect(async () => {
  const count = await page.locator('.btn-primary').count()
  console.log('matches:', count) // 0 or N reveals selector trouble
}).toPass()
Try it live
📊 Production Insight
Half of all 'slow app' timeout escalations end as one-line locator fixes once someone reads the timeline. Triage first, escalate second.
🎯 Key Takeaway
Resolution-spin means fix the selector; world-waiting means fix the app or data — the trace shows which.

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.

test.slow() 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.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.

📊 Production Insight
Timeout config is a budget ledger. Every increase should cite trace evidence the way every expense cites a receipt.
🎯 Key Takeaway
Test, expect, action, navigation — widen the narrowest layer the trace implicates, with a documented reason.

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.

tests/report-timeout-fixed.spec.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Fix the phase: deterministic data + web-first assertions
import { test, expect } from '@playwright/test'

test('report totals', async ({ page }) => {
  // Seed via API in fixtures, not through the UI: setup takes ms, not s
  await page.goto('/reports')
  await page.getByRole('button', { name: 'Run report' }).click()
  // Polls until the text arrives (or the explicit timeout expires)
  await expect(page.getByTestId('report-total')).toHaveText('$42,000', {
    timeout: 15_000, // justified: aggregation endpoint p95 ~12s
  })
})

// Stub the slow third party instead of timing it
// await page.route('**/analytics/*', (route) => route.fulfill({ json: [] }))
Try it live
⚠ Never Raise a Timeout You Cannot Justify From a Trace
A passing test on a raised global timeout proves nothing except that the suite got slower. The only acceptable timeout change is one where the trace names the phase, the cause is understood, and the number has a comment saying both.
📊 Production Insight
Most timeout 'fixes' are budget increases papering over UI-driven setup. API seeding routinely deletes more seconds than any timeout change grants.
🎯 Key Takeaway
Shrink the work (API seeding, stubs), synchronize by polling, then budget only the proven-slow remainder.

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.

tests/trace-workflow.shJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Trace viewer workflow for a timed-out test
// 1. Re-run with tracing forced on:
//    npx playwright test reports.spec.js --trace on
// 2. Open the report and click the trace icon:
//    npx playwright show-report
// 3. In the viewer, walk: timeline -> film strip -> action details
//    - timeline: which step consumed the budget (bars don't lie)
//    - film strip: what the page looked like before/after each action
//    - network tab: which requests were slow or never finished
//    - console tab: errors the app logged while the test waited

// Permanent config: traces on first retry (CI default pattern)
// use: { trace: 'on-first-retry' }
// View any trace file directly:
// npx playwright show-trace test-results/.../trace.zip
Try it live
📊 Production Insight
CI failures with attached traces get fixed in one pass; without them, in three. Trace retention is the cheapest debugging investment in the suite.
🎯 Key Takeaway
Timeline names the culprit, film strip shows the symptom, network tab proves the cause — work them in order.

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 test.slow() 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.

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.

📊 Production Insight
Duration trends are performance monitoring you already own. A suite that tracks them catches app slowdowns before users do.
🎯 Key Takeaway
Budget from measured p95s, keep one behavior per test, and treat duration drift as a regression.
● Production incidentPOST-MORTEMseverity: high

The Team That Doubled Every Timeout and Still Timed Out

Symptom
Timeout failures climbed weekly across unrelated specs. Each was 'fixed' by bumping timeouts until the config allowed 60 seconds and CI stretched past an hour — while failures continued at the same rate, now more expensive.
Assumption
The team assumed every timeout meant 'the app is slow under CI load' and that raising the budget was the responsible fix. Nobody opened a trace for weeks, so nobody saw that several failures burned the budget on locator resolution — a code problem wearing a performance costume.
Root cause
Roughly half the timeouts came from selectors matching hidden or removed elements after a UI refactor — the actions spun on resolution until the 30s budget died. The other half came from journey tests that built their data through the UI on an overloaded staging backend. Raising the timeout to 60s helped neither: resolution spin still consumed the whole budget, and the journeys just failed slower.
Fix
They froze global timeout changes behind trace evidence in review, fixed a dozen bad locators, seeded heavy test data via API instead of UI setup, and split three journey tests. Suite p95 dropped from 24 to 9 minutes, timeout failures vanished, and the one genuinely slow report flow got a documented test.slow() with an owner.
Key lesson
  • 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.
Production debug guideFive checks that find which phase burned the budget before you spend more of it.5 entries
Symptom · 01
Timeout of 30000ms exceeded with no obvious cause
→
Fix
Re-run with tracing forced on (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.
Symptom · 02
One action burns the whole budget resolving its target
→
Fix
Copy the failing locator into the trace viewer's locator picker or a quick 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.
Symptom · 03
Network calls consume most of the 30 seconds
→
Fix
Seed the test data deterministically (API setup or storage state), stub the slow third party with page.route, and replace sleep-based waits with web-first assertions. Re-run: if the same test drops from 28s to 6s, the app path — not the timeout — was the problem.
Symptom · 04
Action finishes fast but expect() burns the remaining time
→
Fix
Replace waitForTimeout 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.
Symptom · 05
Test does five things and totals near 30s with no headroom
→
Fix
Split the journey: move login and seeding into fixtures/storage state, keep one behavior per test, and give legitimately slow paths (uploads, migrations) their own test.slow() or test.setTimeout(). Each test should finish with headroom, not brush the ceiling.
Playwright 30000ms Timeouts — Root Cause vs Fix at a Glance
Root CauseHow to ConfirmFixPrevention
App or backend genuinely slowTrace shows one step consuming most of the 30s; network panel confirmsFix the backend or seed data; raise timeout only with measured causeTrack step durations; alert when p95 drifts upward
Selector never resolves (wrong or hidden target)Trace shows the action spinning on locator resolution, not on loadCorrect the locator; prefer getByRole/getByTestIdRun strict-mode-safe locators; review trace on first failure
Assertion on state that arrives lateAction completes fast, expect() burns the remaining budgetUse web-first assertions with explicit timeoutsNever use waitForTimeout for state synchronization
Test does too much (journey in one test)Trace shows many phases; total near 30s with no headroomSplit into focused tests with shared fixturesOne behavior per test; setup in fixtures
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
playwright.config.jsexport default defineConfig({What Timeout of 30000ms Exceeded Means
teststimeout-triage.spec.jsawait page.locator('.btn-primary').click() // 47 matches? hidden? detached?Slow Selector or Slow App
testsreport-timeout-fixed.spec.jstest('report totals', async ({ page }) => {Fix the App and the Locator Before Raising Timeouts

Key takeaways

1
30s default covers test body plus fixtures and hooks
read the trace before spending more.
2
Slow selector and slow app need opposite fixes; the trace timeline tells them apart.
3
Adjust the narrowest timeout layer that matches the slow phase, never the global first.
4
Web-first assertions poll for state; waitForTimeout guesses at it
prefer polling.
5
One behavior per test with shared fixtures keeps every test far from the 30s ceiling.
6
Track step durations over time so slowdowns surface as regressions, not surprises.

Common mistakes to avoid

5 patterns
×

Raising the global timeout instead of diagnosing

Symptom
Suite passes but takes twice as long, and genuine regressions (a page getting slower every release) hide inside the generous budget.
Fix
Raise 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

Symptom
Same disease as Cypress's cy.wait(ms): every run pays the full delay while slow days still exceed it — flaky and slow simultaneously.
Fix
Use web-first assertions (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

Symptom
Timeouts with huge variance run to run — the UI is fine but the test times an overloaded staging API with cold caches.
Fix
Settle the data before measuring: seed the database, stub the slow third party, or wait for the expected response. Timeouts measure readiness only when the backend is deterministic.
×

Stuffing an entire user journey into one test

Symptom
A 28-second test that times out at 30 with no headroom — any slower day kills it, and the trace shows five unrelated phases each consuming seconds.
Fix
Split it: keep the setup in a shared authenticated fixture or storage state, and assert the flow in focused tests. Each test should time one behavior, not an entire user journey.
×

Treating every timeout as 'the app is slow'

Symptom
Timeout budget grows while the real culprit — a selector matching a hidden element, a request never fired — stays invisible without its fix.
Fix
Scope expectations with explicit locators and per-assertion timeouts ({ timeout: 10000 }) on the slow step. Let the trace tell you which step is slow before you budget for it.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does 'Timeout of 30000ms exceeded' mean in Playwright?
Q02JUNIOR
Name the Playwright timeout layers and their defaults.
Q03SENIOR
How do you tell a slow selector from a slow app in a timed-out test?
Q04SENIOR
Walk me through debugging a timeout with the trace viewer.
Q05SENIOR
How do you design timeout policy for a large Playwright suite?
Q01 of 05JUNIOR

What does 'Timeout of 30000ms exceeded' mean in Playwright?

ANSWER
Each test gets 30 seconds by default, covering the test body plus fixtures and hooks. When the budget expires Playwright aborts with 'Timeout of 30000ms exceeded'. I would open the trace to see which phase burned the time before changing anything.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is the default Playwright test timeout?
02
test.setTimeout vs expect timeout vs actionTimeout — which do I change?
03
What does test.slow() do?
04
Will adding retries fix my timeout failures?
05
How do I use the trace viewer to debug a timeout?
06
How do I give one project or file a longer timeout?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Everything here is grounded in real deployments.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's Playwright. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Cypress Flaky Tests: Why You Should Not Use cy.wait(ms)
1 / 3 · Playwright
Next
Playwright Strict Mode Violation: Locator Resolved to N Elements
→