QA How-To
How to Fix Playwright Test timeout of 30000ms exceeded
Fix Playwright Test timeout of 30000ms exceeded with trace-led diagnosis, correct waits, fixture budgets, CI debugging, and practical TypeScript fixes.
19 min read | 2,841 words
TL;DR
The default Playwright test budget is 30,000 ms and includes fixture setup, `beforeEach`, and the test body. Diagnose the elapsed timeline, fix the blocked state or synchronization, and increase the narrow timeout only when the scenario is intentionally slow.
Key Takeaways
- Treat the 30-second error as an outer test-budget failure, not automatically as a slow click.
- Use the call log and trace timeline to find which hook, fixture, wait, or action consumed the budget.
- Replace fixed sleeps with locators, web-first assertions, and event promises tied to observable product state.
- Register network, popup, and download waits before the action that can trigger them.
- Give legitimately slow fixtures or tests a narrow explicit timeout instead of raising every suite limit.
- Reproduce CI-only failures with matching workers, browser, data, and dependencies before changing timing.
The direct way to fix playwright Test timeout of 30000ms exceeded is to identify which operation consumed the test budget, correct that wait or dependency, and increase the test timeout only when the scenario is legitimately slow. Playwright Test gives each test 30,000 ms by default, and that budget includes the test function, fixture setup, and beforeEach hooks.
A timeout is therefore a symptom, not a diagnosis. The last line in the call log, the trace timeline, and the difference between test, action, assertion, navigation, and fixture timeouts tell you where to look. This guide provides a repeatable investigation workflow, production-ready configuration, and interview-level explanations.
TL;DR
- Reproduce the failure alone and read the complete call log.
- Capture a trace and find the operation that used the remaining budget.
- Replace fixed sleeps and ambiguous locators with state-based waits.
- Separate slow setup into an appropriately timed fixture.
- Raise
test.setTimeout()or configtimeoutonly after proving the duration is expected.
| Message or symptom | Budget involved | Best first check |
|---|---|---|
Test timeout of 30000ms exceeded |
Whole test | Trace timeline, hooks, fixtures, unresolved promises |
expect... timeout 5000ms |
One web-first assertion | Actual UI state and assertion target |
locator.click: Timeout... |
Action, still bounded by test | Locator strictness and actionability log |
page.goto: Timeout... |
Navigation | URL, load event, server, long-lived requests |
| Suite stops after a configured duration | Global run | Infrastructure and globalTimeout |
1. What the 30000ms Test Timeout Really Measures
The default 30-second value belongs to the Playwright Test runner. It is not simply a click timeout. The clock starts for the test's execution budget and includes setup performed by test-scoped fixtures and beforeEach hooks, followed by the test body. If an authentication fixture spends 24 seconds and the first page action spends 7 seconds, the error can appear on that page action even though setup created most of the delay.
After the test function finishes, fixture teardown and afterEach hooks share a separate timeout budget equal to the configured test timeout. beforeAll and afterAll hooks also have their own timeout, initially equal to the test timeout. This distinction matters when the report points to a hook rather than a test.
The default assertion timeout is normally 5 seconds. Low-level action and navigation timeouts have no separate limit by default, but they cannot outlive the remaining test budget. That is why a click may report that it was interrupted after only a few seconds near the end of a test.
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 30_000,
expect: { timeout: 5_000 },
use: {
actionTimeout: 0,
navigationTimeout: 0,
},
});
Treat these as nested budgets. A local assertion limit controls how long that assertion retries, while the test limit remains the outer deadline.
2. How to Fix Playwright Test timeout of 30000ms exceeded Systematically
Start with evidence. Run only the failing test in one worker so parallel load and unrelated failures do not blur the signal. A repeatable command uses the file, line number, project, and a single worker.
npx playwright test tests/checkout.spec.ts:42 --project=chromium --workers=1
npx playwright test tests/checkout.spec.ts:42 --project=chromium --workers=1 --repeat-each=10
npx playwright test tests/checkout.spec.ts:42 --debug
Classify the last incomplete operation. Was Playwright waiting for a locator, a response, a URL, a download, a popup, or a custom promise? Then move backward through the trace and account for the elapsed time. Do not assume the last operation caused the delay. It may simply be where the outer budget expired.
Use a short decision sequence:
- If the locator never matches, inspect the selector, frame, page state, and test data.
- If it matches but cannot act, inspect visibility, stability, enabled state, and overlays.
- If a web-first assertion sees the wrong value, verify the application event that should produce it.
- If a network wait never resolves, inspect the predicate and register the wait before the triggering action.
- If setup dominates, redesign or separately time the fixture.
- If the trace shows steady, valid progress for an intentionally long operation, grant that scenario more time.
This investigation style scales better than adding 60 seconds everywhere. For element-specific waiting failures, use the Playwright visible, enabled, and stable troubleshooting guide.
3. Read the Call Log and Trace Before Changing Code
The call log records what Playwright was waiting for. A line such as waiting for getByRole('button', { name: 'Pay' }) suggests zero matches or a target that never became actionable. Repeated messages about an intercepting element point to an overlay. A pending waitForResponse with no matching request points to timing or an incorrect predicate.
Enable traces on the first retry so routine successful runs stay light while flaky failures preserve evidence.
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Open the generated trace with npx playwright show-trace path/to/trace.zip. Review the action list, DOM snapshot, network panel, console messages, and source location around the long gap. A screenshot alone cannot reveal whether the target changed five times, a request failed, or a hook used most of the clock.
Named steps make timing and ownership obvious in reports:
await test.step('submit the order and confirm persistence', async () => {
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByRole('heading', { name: 'Order confirmed' }))
.toBeVisible();
});
Also inspect browser console and API failures. A frontend exception may prevent the expected UI state forever, and a 500 response can leave a spinner visible until the test ends. In that case, a longer timeout only postpones useful feedback.
4. Replace Sleeps with Observable State
page.waitForTimeout() waits for time, not readiness. A two-second sleep is too long when the update takes 100 ms and too short when CI takes 2.5 seconds. It also consumes the same outer test budget that you are trying to protect. Use Playwright actions and web-first assertions, which retry against a meaningful condition.
import { test, expect } from '@playwright/test';
test('publishes an article', async ({ page }) => {
await page.goto('/editor');
await page.getByLabel('Title').fill('Release notes');
await page.getByRole('button', { name: 'Publish' }).click();
await expect(page.getByRole('status')).toHaveText('Published');
await expect(page).toHaveURL(/\/articles\/[^/]+$/);
});
Avoid waitForLoadState('networkidle') as a generic readiness signal. Applications with analytics, polling, streaming, or background requests may never become idle in a useful sense. Wait for the heading, button state, URL, response, or persisted record that defines the user-visible outcome.
Likewise, do not replace a retrying assertion with a one-time value read:
// Fragile: captures one moment.
expect(await page.getByRole('status').textContent()).toBe('Published');
// Reliable: retries until the expected state or assertion deadline.
await expect(page.getByRole('status')).toHaveText('Published');
Choose the condition from the product requirement. If the requirement says a report becomes downloadable, assert the enabled Download control or capture the download event. If it says data is saved, verify the response and a durable UI state. Waiting for an arbitrary spinner to disappear is weaker when the spinner is unrelated or missing after an error.
5. Fix Locators and Actionability Bottlenecks
A locator timeout frequently comes from ambiguity or a control that never becomes usable. Prefer role, label, text, or a stable test ID, then scope repeated elements to a meaningful container. Locator objects are reevaluated as the DOM changes, which is safer than retaining an old element handle through a rerender.
const invoiceRow = page.getByRole('row')
.filter({ has: page.getByRole('cell', { name: 'INV-1042' }) });
const approve = invoiceRow.getByRole('button', { name: 'Approve' });
await expect(approve).toBeVisible();
await expect(approve).toBeEnabled();
await approve.click();
await expect(invoiceRow.getByRole('cell', { name: 'Approved' })).toBeVisible();
The assertions separate two contracts: the control must be ready, then the approval result must appear. If the first fails, inspect CSS visibility, disabled semantics, animation, and overlays. If the second fails, inspect the business request and resulting state.
Do not use force: true as a standard timeout fix. It bypasses some actionability protection and can create a green test for a control that a user cannot click. Do not use .first() merely to silence a strictness violation either. Scope the locator unless order is the explicit requirement.
If the UI is rendered inside an iframe, enter it through frameLocator. If a click opens a popup, register page.waitForEvent('popup') before clicking. These context boundaries are common reasons a visually correct locator never resolves. Review Playwright getByRole patterns for semantic locator design.
6. Synchronize Network Events Without Races
Network waits must begin listening before the action that can trigger the event. Starting waitForResponse after the click creates a race: a fast response may already be complete. Store the promise first, perform the action, and then await the promise.
import { test, expect } from '@playwright/test';
test('creates an invoice', async ({ page }) => {
await page.goto('/invoices/new');
await page.getByLabel('Customer').fill('Northwind');
const createResponse = page.waitForResponse(response =>
response.url().endsWith('/api/invoices') &&
response.request().method() === 'POST'
);
await page.getByRole('button', { name: 'Create invoice' }).click();
const response = await createResponse;
expect(response.status()).toBe(201);
await expect(page.getByRole('status')).toHaveText('Invoice created');
});
Keep predicates precise but not environment-fragile. Comparing an entire URL may fail when the host or query order changes. Matching only /api may accidentally capture an unrelated request. Combine the pathname, method, and a known response property when necessary.
Do not wait for a response that the action does not always send. A cached query, client-side validation failure, service worker, or changed endpoint can make the promise permanent until the test deadline. Trace the actual network exchange and align the test with the application contract.
For API setup and postcondition checks that do not need a browser, an APIRequestContext is faster and clearer. See how to use Playwright APIRequestContext for isolated request fixtures, authentication, and cleanup.
7. Budget Hooks and Fixtures Correctly
Slow authentication, tenant provisioning, database reset, or artifact upload often hides in fixtures. A test can time out at its first visible line because the fixture already consumed the budget. Keep test-scoped setup small, reuse worker-scoped state only when isolation remains correct, and give a truly slow fixture its own explicit timeout.
import { test as base, expect } from '@playwright/test';
type Fixtures = {
seededProject: { id: string };
};
export const test = base.extend<Fixtures>({
seededProject: [async ({ request }, use) => {
const response = await request.post('/api/test-projects', {
data: { name: `e2e-${Date.now()}` },
});
await expect(response).toBeOK();
const project = await response.json() as { id: string };
await use(project);
await request.delete(`/api/test-projects/${project.id}`);
}, { timeout: 60_000 }],
});
The separate fixture timeout does not excuse inefficient setup. Measure it, minimize repeated work, and preserve test independence. Avoid creating all suite data through the UI when the behavior under test starts after setup. API-based provisioning can reduce duration and failure surface while the UI test still verifies the intended journey.
When a beforeEach must extend the current test budget, use testInfo.setTimeout(testInfo.timeout + 15_000). Use that sparingly and document why. Massive global hooks make ownership unclear and cause every test to pay for setup it may not need.
Teardown deserves the same care. Cleanup should tolerate partially created state and use bounded operations. A teardown hang can obscure the original failure or hold a worker long after useful evidence has been collected.
8. Configure Timeouts to Fix Playwright Test timeout of 30000ms exceeded
Use the smallest configuration scope that matches the reason. A slow report export may justify a longer timeout for one test. A suite of real-time media tests may justify a project-level setting. One overloaded CI machine does not justify hiding every regression behind five-minute defaults.
| Mechanism | Scope | Appropriate use |
|---|---|---|
expect(locator).toHaveText(..., { timeout }) |
One assertion | A known asynchronous state transition |
locator.click({ timeout }) |
One action | A deliberately bounded action diagnostic |
test.setTimeout(ms) |
Current test | One legitimately long scenario |
test.slow() |
Current test | Marks slow and triples its timeout |
Config timeout |
Every test in config or project | A suite with a justified common budget |
Fixture { timeout } |
One fixture definition | Slow setup or teardown with separate ownership |
globalTimeout |
Whole run | Stops a stuck or unhealthy job |
test('exports a large audited report', async ({ page }) => {
test.setTimeout(90_000);
await page.goto('/reports/audit');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
await expect(download.suggestedFilename()).toMatch(/\.csv$/);
});
test.slow() is readable when slowness is an accepted property, not an unexplained failure. Avoid calling page.setDefaultTimeout() deep inside a helper because it changes subsequent actions on that page and makes timing hard to reason about. Prefer explicit local options or central config.
9. Diagnose CI-Only 30000ms Failures
If a test passes locally but fails in CI, first reproduce the CI shape: same browser project, headless mode, worker count, retries, environment variables, data, and service versions. CI-only does not automatically mean "add more time." It often means shared-resource contention, missing fonts, an unavailable dependency, or a race exposed by faster or slower scheduling.
Compare traces from passing and failing runs. Look for a long server response, CPU-heavy animation, repeated authentication, throttled database, browser crash, incorrect base URL, or a modal that appears only in that environment. Preserve console logs and application server logs with the same run identifier.
Reduce workers temporarily to test the contention hypothesis:
npx playwright test --workers=1
If one worker passes and eight fail, inspect rate limits, shared accounts, database locks, fixed ports, and tests mutating common records. Do not leave one worker as the only fix unless serial execution is a real constraint. Isolate state or partition resources so concurrency remains safe.
Retries are a diagnostic and resilience feature, not proof of reliability. Track the tests that pass only on retry. A retry can capture the trace needed to fix a race, but permanently accepting flakes makes the suite slower and reduces trust.
For comprehensive pipeline controls, read Playwright in GitHub Actions. Keep a global run timeout as a final safety boundary, while preserving enough test-level evidence to identify the first meaningful failure.
10. A Sustainable Timeout Policy for Teams
A good policy makes timeouts intentional and reviewable. Define a normal test budget, a narrow assertion budget, and named exceptions for known long journeys. Require a reason when a pull request increases a timeout. The reason should describe the expected event and measured behavior, not "CI is flaky."
Track duration by test and step. Sudden growth can reveal a backend regression before the test crosses 30 seconds. Split end-to-end scenarios that validate unrelated outcomes, but do not split a coherent transaction merely to reset the clock. Faster tests come from focused scope and efficient setup, not from hiding one journey across arbitrary cases.
Build helpers around outcomes rather than sleeps. submitOrderAndWaitForConfirmation() can register the response, click, validate status, and assert the confirmation region. wait(5000) communicates nothing about the product. Keep helpers transparent enough that traces and stack locations still identify the stalled operation.
Finally, classify timeout defects in reviews:
- Product defect: the required state never arrives or the UI remains blocked.
- Test defect: incorrect locator, missed event, leaked promise, or invalid data.
- Environment defect: dependency, resource, or configuration failure.
- Expected duration: a legitimate operation needs a documented exception.
That classification prevents timeout changes from becoming reflexive. It also gives interviewers and engineering leaders confidence that an SDET can separate synchronization from performance and infrastructure concerns.
Interview Questions and Answers
Q: What does "Test timeout of 30000ms exceeded" mean in Playwright?
It means the test runner's outer execution budget expired. The budget includes the test body, test-scoped fixture setup, and beforeEach work. The operation named last may be the cause, or it may only be where the remaining time reached zero, so I confirm with the trace timeline.
Q: What is the difference between test timeout and expect timeout?
The test timeout limits the overall test execution. The expect timeout limits how long one retrying assertion waits for its condition. Increasing the expect timeout cannot extend the outer test deadline, and a long test may interrupt an assertion before its own timeout is reached.
Q: Why is waitForTimeout() a poor fix?
It waits for a duration rather than an observable condition. It slows every successful run, still races on slower runs, and consumes the test budget. I use locator actions, web-first assertions, or event promises tied to the required state.
Q: How do you investigate a timeout that happens only in CI?
I reproduce the CI configuration, capture a trace on retry, and compare timing with a passing run. I test worker contention, validate services and environment data, and correlate browser console, network, and server logs. I increase time only when evidence shows expected work is healthy but legitimately slower.
Q: When is test.slow() appropriate?
It is appropriate when slowness is an accepted property of a specific scenario and marking that property improves reporting. It triples the test timeout. I do not use it to avoid diagnosing a locator, dependency, or synchronization failure.
Q: How should a test wait for a response triggered by a click?
Create the waitForResponse promise before the click, then await the response after the click. The predicate should identify the intended URL and HTTP method closely enough to avoid unrelated traffic. This ordering removes the race where a fast response finishes before the listener exists.
Q: Can an action have no configured timeout and still fail at 30 seconds?
Yes. Action timeout defaults to no separate limit, but the action runs inside the test's outer timeout. When that outer budget expires, the runner interrupts the action. This nested-budget model explains why the call log can name a click in a test-timeout failure.
Q: How do fixtures affect timeout design?
Test-scoped fixture setup uses the test budget by default, so expensive provisioning can starve the test body. A fixture can receive a separate timeout in its definition, but I also optimize and scope it appropriately. Cleanup should be bounded and resilient to partial setup.
Common Mistakes
- Increasing the global test timeout before identifying the blocked operation.
- Reading only the final error line and ignoring elapsed time in hooks and fixtures.
- Adding
waitForTimeout()after clicks instead of asserting the resulting state. - Starting
waitForResponse,waitForEvent, or popup waits after the triggering action. - Using
force: trueto hide an overlay or disabled control that blocks real users. - Setting broad default timeouts inside page objects, which creates hidden side effects.
- Treating all CI-only failures as slow machines without testing shared-state contention.
- Enabling retries and then ignoring tests that pass only on retry.
- Waiting for
networkidlein applications with continuous background traffic. - Giving setup a large fixture timeout without measuring or simplifying it.
Conclusion
To fix playwright Test timeout of 30000ms exceeded reliably, find the exact budget and operation that expired, then repair the state synchronization, locator, fixture, network wait, product behavior, or environment that caused it. A longer timeout is correct only when the trace shows valid progress for an intentionally slow scenario.
Reproduce one failure in a single worker, open its trace, and account for the full 30 seconds. Replace the first arbitrary sleep or ambiguous wait you find with an observable product condition. That small discipline turns timeout handling from guesswork into engineering.
Interview Questions and Answers
What does Test timeout of 30000ms exceeded mean in Playwright?
It means the Playwright Test outer execution budget expired. That budget includes the test body, test-scoped fixture setup, and `beforeEach` work. I inspect the trace because the last interrupted action may only be where the remaining time reached zero.
How do test, action, and assertion timeouts interact?
The test timeout is the outer deadline. Action and assertion timeouts are narrower limits for individual operations, but they cannot outlive the remaining test budget. I configure the smallest scope that represents the expected wait.
What is your first step when a test times out only in CI?
I preserve a trace and match the CI execution shape, including browser, workers, data, and services. Then I compare timing with a passing run and test resource contention. I change a timeout only after identifying healthy expected work that needs more time.
Why are fixed waits considered flaky?
They assume duration instead of checking readiness. The wait is wasteful when the state arrives early and insufficient when it arrives late. Playwright locators, web-first assertions, and event promises provide state-based synchronization.
How do you avoid a race around waitForResponse?
I create the response promise before the action that triggers the request. After the action, I await the registered promise and assert the response. The predicate includes enough path and method detail to avoid unrelated traffic.
When would you use test.slow instead of test.setTimeout?
I use `test.slow()` when being slow is a meaningful test annotation and tripling the configured budget is acceptable. I use `test.setTimeout()` when the scenario needs a specific documented budget. Neither replaces root-cause analysis.
How should slow fixtures be handled?
I first minimize repeated work and choose the correct test or worker scope. If the fixture remains legitimately slow, I give it an explicit fixture timeout and own cleanup in teardown. I measure setup separately so it does not hide test regressions.
How do retries relate to timeout failures?
Retries can preserve evidence and absorb rare infrastructure noise, but a pass on retry is still a flake signal. I track retry-only passes and use the retry trace to diagnose the race. Retries are not a substitute for deterministic state and isolated data.
Frequently Asked Questions
Why does Playwright say Test timeout of 30000ms exceeded?
Playwright Test gives each test a 30-second execution budget by default. The test body, test-scoped fixture setup, and `beforeEach` work can consume that budget, so the final operation is not always the original cause.
How do I increase the timeout for one Playwright test?
Call `test.setTimeout(60_000)` inside that test, or use `test.slow()` when slowness is an accepted property and tripling the timeout is appropriate. Keep the exception local and document the expected slow event.
What is the difference between test timeout and expect timeout?
Test timeout limits the overall test execution. Expect timeout limits one retrying assertion, and it cannot extend the outer test deadline if the test has already used most of its budget.
Should I use page.waitForTimeout to fix a timeout?
Usually no. A fixed sleep waits for elapsed time rather than readiness, slows successful runs, and still fails when the environment is slower. Wait for a locator state, event, response, URL, or business outcome instead.
How can I debug a Playwright timeout in CI?
Capture a trace on the first retry and reproduce the same browser, worker count, data, and environment locally when possible. Compare passing and failing timelines, then inspect network, console, shared resources, and dependency health.
Does test.slow fix a flaky test?
`test.slow()` marks the test as slow and triples its timeout, but it does not repair a race, wrong locator, or failed dependency. Use it only after evidence shows the scenario makes valid progress and legitimately needs more time.
Can fixture setup cause the 30000ms test timeout?
Yes. Test-scoped fixture setup shares the test budget by default, so long provisioning can leave little time for the visible test steps. Optimize setup or assign a separate fixture timeout when that ownership is intentional.
Related Guides
- How to Debug a failing test in VS Code in Playwright (2026)
- How to Fix Playwright Timeout of 30000ms Exceeded
- How to Fix Playwright element is outside of the viewport
- How to Fix Playwright locator resolved to hidden element
- How to Fix Playwright waiting for element to be visible enabled and stable
- How to Add CI to a test framework (2026)