Our angle, stated plainly: we are Geonode and we sell proxies, and Playwright is frequently run through them for scraping and geographic testing. The honest note is that the overwhelming majority of Playwright timeouts have nothing to do with proxies. A selector matching nothing, an element covered by a cookie banner, an animation that never settles — these produce the same error whether your traffic goes direct or through six intermediaries. There is a genuine proxy-related case, covered near the end: residential connections add real latency, so defaults tuned for local testing produce false failures. But check the selector first. If your test fails identically without the proxy configured, the proxy is not the problem.
The Six Timeouts and Their Defaults
The first thing to understand is that these are separate mechanisms with different defaults, and knowing which one fired tells you where to look.
| Timeout | Default | Set via |
|---|---|---|
| Test | 30,000 ms | testConfig.timeout, test.setTimeout() |
| Expect | 5,000 ms | testConfig.expect.timeout, per-assertion option |
| Action | No timeout | testOptions.actionTimeout, per-call option |
| Navigation | No timeout | testOptions.navigationTimeout, per-call option |
| beforeAll / afterAll hook | 30,000 ms | test.setTimeout() inside the hook |
| Global | None | testConfig.globalTimeout |
Values from the Playwright timeouts documentation.
Two of these surprise people.
Action and navigation have no timeout by default. They are bounded only by the test timeout. So an unqualified page.click() will wait up to the remaining test budget, and the error you get is the test timing out rather than the click. That is why the message says 30000ms even though nobody configured a 30-second click.
The global timeout has no default at all. The docs describe its purpose as preventing "excess resource usage when everything went wrong" — worth setting in CI so a hung suite fails rather than occupying a runner indefinitely.
What "Timeout of 30000ms exceeded" Actually Means
That exact message is the test timeout, and the test timeout is a budget rather than a diagnosis. Something inside it took too long, and the message names the budget, not the culprit.
Ranked by how often each is the real cause:
1. A locator matched nothing. The selector is wrong, or the element has not appeared, or it is inside an iframe or shadow root you did not account for. Playwright waits patiently for something that will never exist.
2. The element exists but is not actionable. Covered by an overlay, a cookie banner, or a sticky header. Disabled. Still animating. Playwright waits for it to become clickable, and it never does.
3. A navigation never completed. A network request that hangs, a redirect loop, or a waitUntil condition — networkidle in particular — that a page with persistent connections will never satisfy.
4. An assertion never became true. An expect polling for a condition the application does not reach.
5. The test genuinely does too much. Real, and the least common.
The order matters because the fix differs completely. Only case 5 is solved by raising the timeout. In the other four, raising it means waiting longer for the same failure.
Actionability Is Why Your Click Waits
Understanding this removes most of the confusion, because it explains what Playwright is doing during those thirty seconds.
The actionability documentation states that Playwright "performs a range of actionability checks on the elements before making actions to ensure these actions behave as expected", and that it "auto-waits for all the relevant checks to pass and only then performs the requested action". When the checks are not satisfied in time, "the action fails with the TimeoutError."
The checks required differ by action, and that difference is diagnostic:
| Action | Checks required |
|---|---|
click, dblclick, check, uncheck, tap, setChecked | visible, stable, receives events, enabled |
hover, dragTo | visible, stable, receives events |
fill, clear | visible, enabled |
selectOption | visible, enabled |
screenshot, selectText | visible |
scrollIntoViewIfNeeded | stable |
blur, focus, press, pressSequentially, dispatchEvent, setInputFiles | none |
Two things fall out of this table immediately.
A click that times out while fill on the same element works points at "stable" or "receives events" — the element is moving, or something is on top of it. Animations and overlays are the usual suspects.
Actions with no checks are an escape hatch, and a warning sign. If locator.click() times out but dispatchEvent('click') works, you have not fixed anything — you have bypassed the check that was telling you a real user could not click that element either. Sometimes that is acceptable. Usually it means there is a genuine overlay problem your test just stopped detecting.
Changing Each Timeout in the Right Place
Configuration lives at several levels, and putting it in the wrong one produces confusing results.
Global configuration, in playwright.config.ts:
export default defineConfig({
timeout: 60_000,
globalTimeout: 60 * 60 * 1000,
expect: { timeout: 10_000 },
use: {
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
});
Note where each sits. timeout and globalTimeout are top-level config; expect.timeout is under expect; actionTimeout and navigationTimeout are under use, because they are test options rather than runner configuration. Putting them at the wrong level is silently ignored.
Per test:
test('slow one', async ({ page }) => {
test.setTimeout(120_000);
// ...
});
test.slow() triples the default timeout — a good default for a test you know is genuinely long without picking an arbitrary number.
Per assertion:
await expect(page.getByRole('status')).toHaveText('Done', { timeout: 30_000 });
Per action:
await page.getByRole('button', { name: 'Export' }).click({ timeout: 15_000 });
In beforeAll and afterAll, which have their own 30-second budget, call test.setTimeout() inside the hook itself.
For a slow fixture, give it its own timeout in test.extend() rather than inflating every test that uses it:
export const test = base.extend<{ seeded: void }>({
seeded: [async ({}, use) => {
await seedDatabase();
await use();
}, { timeout: 60_000 }],
});
The general principle: set the narrowest scope that solves the problem. Raising the global test timeout to accommodate one slow test makes every other test slower to fail, which costs real time in CI.
What Counts Against the Test Timeout
Frequently misunderstood, and it explains tests that time out "before doing anything".
The documentation is explicit: "Time spent by the test function, fixture setups, and beforeEach hooks is included in the test timeout."
So a beforeEach that logs in, seeds data and navigates consumes the same 30 seconds your test body needs. A test that appears to time out on its first line may have spent 28 seconds in setup.
Fixtures share the test timeout by default, which is the same trap in a different shape — an expensive fixture eats the budget of every test that depends on it. Give slow fixtures their own timeout instead of raising the test timeout everywhere.
Teardown is separated: fixture teardowns and afterEach hooks get their own budget after the test function completes, so a slow teardown does not consume the test's time.
The practical implication for debugging: when a test times out, look at the whole chain — fixtures, beforeEach, and the test body — not just the line the error points at.
Diagnose Before You Raise
A sequence that resolves most timeouts in a few minutes.
Run with the trace viewer. This is the single highest-value tool and it is underused:
npx playwright test --trace on
npx playwright show-trace trace.zip
The trace shows every action, its duration, DOM snapshots before and after, and the network activity. A locator that matched nothing is immediately visible; so is the cookie banner sitting on top of your button.
Run headed and slowed down when you want to watch it happen:
npx playwright test --headed --debug
Check the locator resolves at all:
console.log(await page.getByRole('button', { name: 'Save' }).count());
Zero means a selector problem, and no timeout value will fix it.
Check whether it is a stability problem by using a check-free action as a diagnostic — not as a fix. If dispatchEvent('click') works where click() times out, something is covering or moving the element.
Check for a networkidle wait. Pages with analytics beacons, websockets or polling may never reach network idle. Prefer waiting for the thing you actually care about:
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
Read the error message fully. Playwright's timeout errors include the locator, the resolved element count, and which actionability check was pending. That last detail usually names the problem outright.
Raising Timeouts Usually Makes Flakiness Worse
The counterintuitive but important part.
A flaky test is one whose outcome depends on timing. Raising the timeout widens the window in which it passes, so the flake becomes rarer — and correspondingly harder to reproduce, harder to diagnose, and slower when it does fail.
Meanwhile the cost is paid on every failure. A suite of 200 tests with a 30-second timeout takes at most 100 minutes to fail completely; at 120 seconds it takes 400. In CI that is real money and real waiting.
What actually fixes flakiness:
Wait for state, not for time. waitForTimeout is almost always wrong. Assert the condition you care about and let Playwright poll.
Use web-first assertions. expect(locator).toBeVisible() retries automatically. expect(await locator.isVisible()).toBe(true) checks once and fails on the first miss — a subtle but very common source of flakiness.
Deal with overlays deterministically. Dismiss cookie banners in a fixture rather than hoping they have gone.
Disable animations in your configuration where you can, rather than waiting for them to settle.
Wait for the specific network response you depend on rather than for the network to go quiet.
Stabilise the data. Tests that depend on shared mutable state are flaky for reasons no timeout addresses.
When to genuinely raise a timeout: the operation really is slow and there is no way around it — a large file upload, a report that takes a minute to generate, a deliberately throttled network profile. In those cases raise it narrowly, on that test or that assertion, and leave the defaults alone.
Timeouts When Running Through a Proxy
The case where a raise is legitimate, and the configuration to go with it.
Playwright takes proxy settings in the network configuration:
export default defineConfig({
use: {
proxy: {
server: 'http://proxy.example.com:9000',
username: 'user',
password: 'pass',
},
},
});
Or per context, which is what you want when different tests need different exit locations:
const context = await browser.newContext({
proxy: { server: 'http://proxy.example.com:9000' },
});
Three practical consequences.
Residential proxies add genuine latency. Traffic exits through a real consumer connection, so several hundred extra milliseconds per request is normal rather than a fault. A page making eighty requests accumulates that eighty times. Defaults tuned against localhost will produce failures that look like broken proxies and are actually just distance.
The right response is to measure rather than guess: run the suite through the proxy, look at the trace timings, and set navigationTimeout and actionTimeout from what you observe with a generous margin.
Bandwidth is the real cost, and it is enormous with a browser. Playwright fetches every image, font, script and video preload. On metered residential traffic at $0.79/GB this dominates everything else you spend. Blocking unnecessary resource types is the single largest saving available:
await page.route('**/*.{png,jpg,jpeg,webp,gif,woff,woff2,mp4}', r => r.abort());
That routinely cuts traffic by most of the total and speeds up your tests as a side effect.
A block is not a timeout. If a target serves a challenge page, Playwright will time out waiting for an element that is not on the challenge page — which looks exactly like a timeout and is not one. Take a screenshot on failure and look at what actually rendered:
use: { screenshot: 'only-on-failure', trace: 'retain-on-failure' }
This is the silent-failure pattern we described in why testing proxies matters: the request succeeded, the page rendered, and it was the wrong page.
People Also Ask
What is the default timeout in Playwright?
30,000 ms for a test and for beforeAll/afterAll hooks, and 5,000 ms for expect assertions. Action and navigation timeouts have no default and are bounded only by the test timeout, which is why a slow click reports the test's 30 seconds rather than its own limit.
How do I increase the timeout for one Playwright test?
Call test.setTimeout(120_000) inside the test, or test.slow() to triple the default. Prefer these over raising the global timeout, which makes every other test slower to fail.
Why does my Playwright test time out when the element is on the page?
Usually because it is not actionable. click requires the element to be visible, stable, receiving events and enabled — so an element covered by a banner, or still animating, will be found but never clicked. The error message names which check was pending.
What is the difference between test timeout and expect timeout?
The test timeout is the total budget for the test function, fixture setups and beforeEach hooks combined, defaulting to 30 seconds. The expect timeout is how long a single web-first assertion polls, defaulting to 5 seconds. An assertion failing after 5 seconds is the expect timeout, not the test timeout.
Should I use waitForTimeout in Playwright?
Almost never. A fixed sleep is either too short, making the test flaky, or too long, making the suite slow — usually both on different machines. Wait for the condition instead with a web-first assertion, which retries automatically until the timeout.
Why does networkidle never resolve?
Because the page keeps making requests — analytics beacons, websockets, polling, long-lived connections. networkidle requires quiet, and many modern applications never go quiet. Wait for the specific element or response you care about instead.
Does raising the timeout fix flaky tests?
It hides them. The flake becomes rarer, harder to reproduce and slower to fail, while every genuine failure in the suite now takes longer. Fix the cause — wait for state rather than time, use retrying assertions, dismiss overlays deterministically, and disable animations.
Do I need longer timeouts when using a proxy?
Often yes, for navigation and actions, because residential proxies add real per-request latency that accumulates across the many requests a page makes. Measure it through the proxy with tracing enabled and set values from what you observe, rather than raising everything preemptively.
Wrapping Up
The message says the test exceeded 30 seconds, and that is a budget rather than an explanation. Something inside it waited for a condition that never arrived, and in four cases out of five that condition is a selector matching nothing or an element that never became actionable.
So the ordering that saves time is: read the full error, which names the pending actionability check; open the trace, which shows you the DOM at the moment of failure; confirm the locator resolves; and only then consider the number. Raising a timeout is the correct fix for exactly one cause — an operation that genuinely takes longer than the budget — and the wrong fix for the other four, where it buys a slower failure.
When you do raise one, raise it narrowly. Per test, per assertion, per fixture. Inflating the global defaults to accommodate a single slow upload makes every failure in the suite more expensive, and CI time is the one resource that never comes back.
