Next-Gen App & Browser Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Playwright tests run slow or time out mainly because of broad or unstable locators that force Playwright to retry until a timeout fires, manual waitForTimeout() hard waits instead of auto-waiting web-first assertions, default timeouts (30s per test, 5s for expect) that are too tight for slow network or CI, running headed instead of headless, no parallelism or sharding, and trace or video overhead on an underpowered runner. The fix is to tune the right timeout type in playwright.config.ts, use stable locators, remove hard waits, and parallelize, ideally on a cloud grid.
Most Playwright slowness traces back to a small set of root causes. Unlike a generic WebDriver suite, Playwright has its own auto-waiting model, layered timeouts, and built-in parallelism, so the causes (and fixes) are Playwright-specific:
Playwright has several distinct timeouts, and raising the wrong one just hides the real problem. Know what each one covers before you change anything:
| Timeout | Default | Where set | What it covers |
|---|---|---|---|
| Test timeout | 30,000ms | timeout | The whole test body |
| Expect timeout | 5,000ms | expect.timeout | Web-first assertions |
| Action timeout | Unset (uses test timeout) | use.actionTimeout | click, fill, and similar actions |
| Navigation timeout | Unset (uses test timeout) | use.navigationTimeout | goto and navigations |
| Global timeout | Unset | globalTimeout | The entire run |
Configure these explicitly in playwright.config.ts so each timeout matches your app's real behavior:
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 60_000, // per-test timeout (default 30_000)
globalTimeout: 30 * 60_000, // whole run cap (CI safety net)
expect: {
timeout: 10_000, // web-first assertions (default 5_000)
},
use: {
actionTimeout: 15_000, // click/fill/etc.
navigationTimeout: 30_000, // goto / navigations
},
});When a single test legitimately needs longer, override it locally instead of relaxing the global value:
test('slow report export', async ({ page }) => {
test.setTimeout(120_000); // or test.slow() to triple it
// ...
});
await expect(page.getByRole('button', { name: 'Submit' }))
.toBeVisible({ timeout: 10_000 });
await page.getByRole('link', { name: 'Reports' }).click({ timeout: 8_000 });Raise the specific timeout for the slow step, not the blanket global test timeout, which only masks the real cause.
The single biggest speed and stability win in Playwright is to stop hard-waiting. Playwright already auto-waits for an element to be actionable before it acts, so a fixed delay is wasted time on every run.
// Anti-pattern: blind 5s wait every run
await page.waitForTimeout(5000);
await page.fill('#username', 'testuser');
// Better: Playwright auto-waits for actionability
await page.getByLabel('Username').fill('testuser');
// Wait for a condition, not a clock
await expect(page.getByTestId('dashboard')).toBeVisible(); // auto-retries
await page.waitForResponse(r => r.url().includes('/api/orders') && r.ok());Web-first assertions like toBeVisible and toHaveText auto-retry until the expect timeout, so they pass the instant the app is ready instead of always burning a fixed delay.
Prefer role-based and accessible locators such as getByRole, getByLabel, and getByTestId over broad CSS or XPath. Broad selectors that match many nodes force Playwright to do extra resolution work and are far more brittle.
// Slow / fragile
await page.locator('//div[@class="content"]//button').click();
// Fast / stable
await page.getByRole('button', { name: 'Checkout' }).click();Playwright runs test files in parallel across worker processes by default. Enable full parallelism and match the worker count to your machine's cores. For background on the approach, see What Is Parallel Testing and Why to Adopt It.
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : undefined, // match CI vCPUs
});To go beyond a single machine, shard the suite across multiple CI runners:
npx playwright test --shard=1/4
npx playwright test --shard=2/4 # on a second runner, and so onThere is a ceiling to what your own hardware can do, which is exactly where a cloud grid takes over.
Capturing artifacts for every test is expensive. Capture them only when a test retries or fails so you keep the debugging value without paying the cost on every green run.
export default defineConfig({
use: {
trace: 'on-first-retry', // not 'on'
video: 'retain-on-failure',
screenshot: 'only-on-failure',
},
retries: process.env.CI ? 2 : 0,
});Logging in through the UI on every test is one of the most common hidden time sinks. Log in once in a global setup, save the authenticated state, and reuse it everywhere.
// global-setup.ts - log in once, save state
import { chromium } from '@playwright/test';
export default async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: 'state.json' });
await browser.close();
};// playwright.config.ts
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
use: { storageState: 'state.json' },
});CI runners have fewer vCPUs and less RAM than your laptop, so set a realistic workers count and slightly higher timeouts for CI only. Just as important, avoid waitUntil: 'networkidle' on pages with persistent connections (analytics, web sockets, long-polling) because the network may never go idle and the navigation will hang until it times out. Prefer 'domcontentloaded' plus a specific assertion:
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();Run headless in CI (headless: true), and use slowMo only for local debugging, never in a pipeline.
Once you have removed hard waits, fixed locators, and tuned timeouts, the remaining bottleneck is usually raw machine capacity. Running parallel and sharded Playwright tests across a cloud grid of browser and OS combinations removes that local-hardware ceiling and cuts wall-clock time dramatically. You can run the same configuration unchanged, just pointed at the cloud. See Parallel Testing with Playwright and the Playwright Testing platform for setup details. For very large suites, HyperExecute can further cut end-to-end execution time by orchestrating runs across many machines at once.
Running Selenium instead? See Why Are Selenium Tests Running Slow or Timing Out?.
Playwright uses a 30,000ms (30s) timeout per test and a 5,000ms (5s) timeout for expect web-first assertions. Action and navigation timeouts are unset by default and fall back to the test timeout. All of these can be changed in playwright.config.ts.
Find the slow step first, then fix the underlying cause: replace a broad locator with getByRole, remove a waitForTimeout hard wait, or wait on a response. Only after that should you raise the specific timeout with test.setTimeout or a per-action timeout, rather than blanket-raising the global test timeout.
No, except for brief local debugging. waitForTimeout() is a blind hard wait that always burns a fixed delay and adds flakiness. Use auto-waiting web-first assertions such as expect().toBeVisible(), or wait on a real signal with waitForResponse or locator.waitFor.
CI runners typically have fewer vCPUs and less RAM than a laptop, and network is often throttled. Tune the workers count to the CI machine, reduce trace and video artifacts, run headless, and offload execution to a cloud grid to remove the local-hardware ceiling.
Run headless, enable fullyParallel with a sensible workers count, shard across machines in CI, reuse authentication with storageState, capture trace and video only on failure or retry, and parallelize across a cloud grid for large suites.
Yes. Headed mode adds rendering and window-management overhead, so it is slower than headless. Keep headless set to true in CI and only run headed when you are actively debugging a test locally.
KaneAI - Testing Assistant
World’s first AI-Native E2E testing agent.

TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance