Debugging August 17, 2026 14 min read

Playwright Timeout Errors: How to Fix Every Timeout Issue (2026)

Timeout errors are the most common source of flaky Playwright tests. Understanding the four distinct timeout types — test, navigation, action, and expect — and knowing when to configure each one is the difference between a reliable test suite and one that fails randomly in CI. This guide covers every timeout scenario you will encounter and how to fix it properly.

Timeout exceeded. If you have used Playwright for more than a day, you have seen this error. It appears in different forms — navigation timeout of 30000ms exceeded, waiting for selector, expect.toBeVisible timed out — but the underlying problem is always the same: Playwright waited for something and it did not happen fast enough.

The mistake most developers make is reaching for a blanket timeout increase or, worse, sprinkling page.waitForTimeout() calls throughout their tests. Both approaches mask the real problem and make tests slower and more fragile. The correct approach is to understand which timeout fired, why it fired, and configure the right timeout at the right level.


Understanding Playwright's Timeout System

Playwright has four independent timeout types. Each one controls a different aspect of test execution, has its own default value, and is configured separately. Mixing them up is the root cause of most timeout confusion.

Timeout Type Default What It Controls Config Key
Test timeout 30,000 ms Total time for the entire test (including all actions, navigations, and assertions) timeout
Navigation timeout 30,000 ms page.goto(), page.waitForURL(), page.reload(), page.goBack() navigationTimeout
Action timeout 0 (no limit) click(), fill(), check(), selectOption(), and all other locator actions actionTimeout
Expect timeout 5,000 ms expect(locator).toBeVisible(), toHaveText(), toHaveURL(), and all web-first assertions expect.timeout

Critical distinction: The action timeout defaults to 0 (no limit). This means page.click('.button') will wait forever for the button to appear — or until the test timeout fires. If you see a test timeout error during a click operation, the fix is usually to set actionTimeout in your config rather than increasing the test timeout.

Test Timeout

The test timeout is the outer boundary — the maximum time an entire test function is allowed to run. It includes all navigations, actions, assertions, and any waiting. When this timeout fires, the test is killed immediately regardless of what it was doing.

Default: 30 seconds

For most unit-style Playwright tests, 30 seconds is more than enough. But end-to-end flows that involve multiple page navigations, file uploads, or slow backend APIs can legitimately need more time.

Setting test timeout globally

TypeScript — playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Global test timeout: 60 seconds
  timeout: 60_000,
});

Setting test timeout per test

TypeScript
import { test, expect } from '@playwright/test';

test('complete checkout flow', async ({ page }) => {
  // Override timeout for this test only
  test.setTimeout(120_000); // 2 minutes

  await page.goto('/products');
  await page.getByRole('button', { name: 'Add to Cart' }).click();
  await page.goto('/checkout');
  // ... rest of checkout flow
});

Using test.slow() to triple the timeout

TypeScript
test('upload large file', async ({ page }) => {
  // Triples the default timeout (30s → 90s)
  test.slow();

  await page.goto('/upload');
  await page.setInputFiles('input[type="file"]', './large-dataset.csv');
  await expect(page.getByText('Upload complete')).toBeVisible();
});

Best practice: Use test.slow() for tests you know are inherently slow (file uploads, video processing, report generation). It communicates intent to your team — this test is slow by design, not by accident. Reserve test.setTimeout() for precise values.

The navigation timeout controls how long Playwright waits for page navigation to complete. It applies to page.goto(), page.waitForURL(), page.reload(), page.goBack(), and page.goForward().

Default: 30 seconds

The most common error you will see is:

Error
page.goto: Navigation timeout of 30000ms exceeded.
==================== logs ====================
navigating to "https://example.com/dashboard", waiting until "load"

The waitUntil option

The key to fixing navigation timeouts is understanding the waitUntil parameter. It controls what Playwright considers a "completed" navigation:

  • 'load' (default) — waits for the load event. This means all resources (images, stylesheets, iframes) must finish loading. The slowest option but the most complete.
  • 'domcontentloaded' — waits for the DOMContentLoaded event. The HTML is parsed and DOM is ready, but images and stylesheets may still be loading. Fast and sufficient for most tests.
  • 'networkidle' — waits until there are no more than 0 network connections for 500ms. Useful for SPAs that load data after the initial page load. Can be unreliable if the page has persistent connections (WebSockets, long-polling).
  • 'commit' — waits only for the network response to be received and the document to start loading. The fastest option. Useful when you just need the page to start rendering.
TypeScript
// Default: waits for 'load' event — all resources loaded
await page.goto('https://example.com');

// Faster: only wait for DOM to be ready
await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded',
});

// For SPAs: wait until network is idle
await page.goto('https://example.com/dashboard', {
  waitUntil: 'networkidle',
});

// Fastest: just wait for response headers
await page.goto('https://example.com', {
  waitUntil: 'commit',
});

// Override navigation timeout for a single call
await page.goto('https://slow-api.example.com', {
  timeout: 60_000, // 60 seconds
});

Setting navigation timeout globally

TypeScript — playwright.config.ts
export default defineConfig({
  use: {
    navigationTimeout: 60_000, // 60s for all navigations
  },
});

Tip: If your app is an SPA (React, Vue, Angular), most "page navigations" are actually client-side route changes that do not trigger a real browser navigation. In that case, page.goto() only fires on the initial load. For subsequent route changes, use page.waitForURL() or expect(page).toHaveURL() instead.

Action Timeout

The action timeout controls how long Playwright waits for locator actions to succeed — click(), fill(), check(), selectOption(), press(), and others. Before performing an action, Playwright runs a series of actionability checks: it waits for the element to be visible, enabled, stable (not animating), and able to receive events.

Default: 0 (no limit)

This is the most surprising default. Actions wait indefinitely — they will only fail when the test timeout expires. This means if page.click('.submit-button') cannot find the button, you will not get a clear "action timed out" error. Instead, you get a generic "test timeout" error that does not tell you which action was stuck.

Setting a sensible action timeout

TypeScript — playwright.config.ts
export default defineConfig({
  use: {
    actionTimeout: 10_000, // 10s — if an element doesn't appear in 10s, fail fast
  },
});

Per-action timeout override

TypeScript
// Override for a single action
await page.getByRole('button', { name: 'Submit' }).click({
  timeout: 15_000, // 15 seconds for this click only
});

// Useful for elements that appear after slow operations
await page.getByText('Processing complete').click({
  timeout: 30_000,
});

Recommendation: Always set actionTimeout in your config. Leaving it at 0 means you never get specific action timeout errors — only vague test timeouts. A value of 10,000ms (10 seconds) is a good starting point. Any element that takes longer than 10 seconds to appear is usually a symptom of a real performance issue or a wrong selector.

Expect/Assertion Timeout

The expect timeout controls how long Playwright's web-first assertions retry before failing. Assertions like expect(locator).toBeVisible(), expect(locator).toHaveText(), and expect(page).toHaveURL() are auto-retrying — they poll the page repeatedly until the condition is met or the timeout expires.

Default: 5 seconds

Five seconds is usually enough for UI elements that are already present or appearing via a fast interaction. But it is too short for assertions that depend on API responses, animations, or server-side processing.

Error
expect(locator).toBeVisible
Timed out 5000ms waiting for expect(locator).toBeVisible()
Locator: getByText('Order confirmed')
Expected: visible
Received: hidden

Setting expect timeout globally

TypeScript — playwright.config.ts
export default defineConfig({
  expect: {
    timeout: 10_000, // 10s for all assertions
  },
});

Per-assertion timeout override

TypeScript
// Wait up to 30 seconds for this specific assertion
await expect(page.getByText('Report generated')).toBeVisible({
  timeout: 30_000,
});

// Wait for URL to change after form submission
await expect(page).toHaveURL(/\/success/, {
  timeout: 15_000,
});

// Wait for text content to update
await expect(page.getByTestId('status')).toHaveText('Complete', {
  timeout: 20_000,
});

Using expect.configure() for scoped timeout changes

TypeScript
import { test, expect } from '@playwright/test';

test('dashboard loads with data', async ({ page }) => {
  // Create a custom expect with a longer timeout
  const slowExpect = expect.configure({ timeout: 20_000 });

  await page.goto('/dashboard');

  // These assertions use the 20s timeout
  await slowExpect(page.getByTestId('chart')).toBeVisible();
  await slowExpect(page.getByTestId('data-table')).toHaveCount(10);

  // Regular expect still uses the default 5s timeout
  await expect(page.getByRole('heading')).toHaveText('Dashboard');
});

Global vs Per-Test vs Per-Action Timeout Configuration

Here is a complete playwright.config.ts showing all four timeout types configured together:

TypeScript — playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // 1. Test timeout — max time per test
  timeout: 60_000, // 60 seconds

  // 2. Expect timeout — max time per assertion
  expect: {
    timeout: 10_000, // 10 seconds
  },

  use: {
    // 3. Action timeout — max time per click/fill/etc
    actionTimeout: 10_000, // 10 seconds

    // 4. Navigation timeout — max time per goto/reload
    navigationTimeout: 30_000, // 30 seconds

    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'api-tests',
      timeout: 120_000, // API tests get 2 minutes
      use: {
        actionTimeout: 0, // No action timeout for API tests
      },
    },
  ],
});

The precedence order from most specific to least specific is:

  1. Per-action/per-assertion.click({ timeout: 5000 }) or .toBeVisible({ timeout: 10000 })
  2. Per-testtest.setTimeout(120_000)
  3. Per-projectprojects: [{ timeout: 60000, use: { actionTimeout: 10000 } }]
  4. Global config — top-level timeout, expect.timeout, use.actionTimeout, use.navigationTimeout
  5. Playwright defaults — 30s test, 0 action, 30s navigation, 5s expect

Debugging Timeout Errors

When a timeout fires, the error message tells you what timed out but not always why. These tools help you find the root cause.

1. Trace Viewer

The Trace Viewer is the most powerful debugging tool for timeout issues. It records a timeline of every action, network request, console log, and DOM snapshot — letting you see exactly what the page looked like at the moment the timeout occurred.

TypeScript — playwright.config.ts
export default defineConfig({
  use: {
    // Record trace on first retry — keeps CI fast while capturing debug data
    trace: 'on-first-retry',
  },
  retries: 1,
});
CLI
# After a failed test, open the trace
npx playwright show-trace test-results/my-test-chromium/trace.zip

# Or view it in the HTML report
npx playwright show-report

In the trace, look for: which action was executing when the timeout fired, what the page looked like (DOM snapshot), which network requests were still pending, and any console errors.

2. Debug mode with --debug flag

Running with --debug opens the Playwright Inspector alongside the browser. You can step through actions one at a time, inspect locators, and see exactly what the test sees.

CLI
# Run a specific test in debug mode
npx playwright test my-test.spec.ts --debug

# Debug a specific test by title
npx playwright test -g "checkout flow" --debug

3. page.pause() for interactive debugging

Insert page.pause() at the point where your test times out. This pauses execution and opens the Playwright Inspector, letting you interact with the page and test locators in real time.

TypeScript
test('debug timeout issue', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByLabel('Card number').fill('4242424242424242');

  // Pause here to inspect the page before the failing action
  await page.pause();

  // This action was timing out — now you can see why
  await page.getByRole('button', { name: 'Pay Now' }).click();
});

4. Screenshots on failure

Playwright captures a screenshot automatically when a test fails. Configure this in your config to always have a visual record of the failure state:

TypeScript — playwright.config.ts
export default defineConfig({
  use: {
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
});

Common Timeout Scenarios and Fixes

Here are the timeout situations you will encounter most often, along with the correct fix for each.

Scenario 1: Slow API response delays page content

The page loads but the content you need depends on an API call that takes 10+ seconds.

TypeScript
// BAD: This assertion times out because the API is slow
await page.goto('/dashboard');
await expect(page.getByTestId('user-table')).toBeVisible(); // ❌ 5s default

// GOOD: Wait for the API response, then assert
await page.goto('/dashboard');
await page.waitForResponse(
  resp => resp.url().includes('/api/users') && resp.status() === 200
);
await expect(page.getByTestId('user-table')).toBeVisible();

Scenario 2: Lazy-loaded content below the fold

Elements that load only when scrolled into view will not appear until you scroll to them.

TypeScript
// BAD: Element exists in DOM but is lazy-loaded — never renders
await expect(page.getByTestId('footer-newsletter')).toBeVisible(); // ❌ timeout

// GOOD: Scroll to the element first
await page.getByTestId('footer-newsletter').scrollIntoViewIfNeeded();
await expect(page.getByTestId('footer-newsletter')).toBeVisible();

Scenario 3: SPA client-side navigation

In SPAs, clicking a link does not trigger a real navigation event. page.waitForNavigation() will time out.

TypeScript
// BAD: SPA route change doesn't trigger navigation event
await Promise.all([
  page.waitForNavigation(), // ❌ times out
  page.click('a[href="/settings"]'),
]);

// GOOD: Wait for URL change instead
await page.click('a[href="/settings"]');
await expect(page).toHaveURL(/\/settings/);

Scenario 4: iframe content

Elements inside iframes require accessing the frame first. Trying to find them on the main page will time out.

TypeScript
// BAD: Element is inside an iframe — not visible on main page
await page.getByRole('button', { name: 'Submit Payment' }).click(); // ❌ timeout

// GOOD: Access the iframe's content frame
const paymentFrame = page.frameLocator('iframe[name="payment"]');
await paymentFrame.getByRole('button', { name: 'Submit Payment' }).click();

Scenario 5: Popup or new tab

Actions that open a new window or popup require waiting for the popup event. Interacting with the original page will not find elements in the new tab.

TypeScript
// BAD: Content is in a new tab — not on the current page
await page.getByText('Open Preview').click();
await expect(page.getByText('Preview Mode')).toBeVisible(); // ❌ timeout

// GOOD: Capture the popup and interact with it
const popupPromise = page.waitForEvent('popup');
await page.getByText('Open Preview').click();
const popup = await popupPromise;
await popup.waitForLoadState();
await expect(popup.getByText('Preview Mode')).toBeVisible();

Scenario 6: Element hidden behind overlay or modal

Playwright's actionability checks ensure an element is not obscured before clicking. If a modal, cookie banner, or loading overlay covers the element, the click will wait until the overlay disappears — and time out if it does not.

TypeScript
// GOOD: Dismiss the overlay first, then click
await page.getByRole('button', { name: 'Accept Cookies' }).click();
await expect(page.locator('.cookie-banner')).toBeHidden();

// Now the target element is no longer obscured
await page.getByRole('button', { name: 'Add to Cart' }).click();

Wait Strategies: The Right Way to Wait

Playwright provides several event-driven waiting methods that are far superior to hard timeouts. Each one waits for a specific condition, making your tests both faster and more reliable.

waitForSelector

Waits for an element matching the selector to appear (or disappear) from the DOM.

TypeScript
// Wait for element to appear in the DOM
await page.waitForSelector('.loading-spinner', { state: 'attached' });

// Wait for element to be visible (default state)
await page.waitForSelector('.results-table');

// Wait for element to disappear (loading state cleared)
await page.waitForSelector('.loading-spinner', { state: 'hidden' });

// Wait for element to be detached from DOM entirely
await page.waitForSelector('.modal-overlay', { state: 'detached' });

waitForLoadState

Waits for the page to reach a specific load state. Useful after actions that trigger resource loading.

TypeScript
// Wait for all network requests to complete
await page.waitForLoadState('networkidle');

// Wait for DOM to be fully parsed
await page.waitForLoadState('domcontentloaded');

// Wait for all resources (images, stylesheets) to load
await page.waitForLoadState('load');

waitForResponse

Waits for a specific network response. This is the most precise wait when your UI depends on an API call.

TypeScript
// Wait for a specific API response before asserting
await page.getByRole('button', { name: 'Search' }).click();

const response = await page.waitForResponse(
  resp => resp.url().includes('/api/search') && resp.status() === 200
);

const data = await response.json();
console.log(`Search returned ${data.results.length} results`);
await expect(page.getByTestId('results')).toBeVisible();

waitForURL

Waits for the page URL to match a pattern. Essential for SPA navigation and form submissions that redirect.

TypeScript
// Wait for redirect after login
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('**/dashboard');

// Wait for URL to match a regex
await page.waitForURL(/\/orders\/\d+/);

// With custom timeout
await page.waitForURL('**/success', { timeout: 15_000 });

waitForFunction

Evaluates a JavaScript function in the browser context and waits until it returns a truthy value. Use this for custom conditions that no built-in wait covers.

TypeScript
// Wait for a JavaScript variable to be set
await page.waitForFunction(() => {
  return (window as any).appReady === true;
});

// Wait for element count to reach a specific number
await page.waitForFunction(() => {
  return document.querySelectorAll('.list-item').length >= 10;
});

Anti-Patterns: What NOT to Do

These patterns are the most common causes of flaky and slow Playwright tests. Avoid all of them.

Anti-Pattern 1: page.waitForTimeout() (hard sleep)

TypeScript
// ❌ BAD: Hard sleep — always waits 3 seconds even if element is ready instantly
await page.goto('/dashboard');
await page.waitForTimeout(3000);
await expect(page.getByTestId('chart')).toBeVisible();

// ✅ GOOD: Event-driven wait — resolves as soon as the chart appears
await page.goto('/dashboard');
await expect(page.getByTestId('chart')).toBeVisible();

page.waitForTimeout() exists only for debugging purposes. It has no place in production test code. It slows down your test suite by the exact amount of the delay on every single run, and it is still flaky — if the page needs 3.1 seconds on a slow CI machine, your 3-second wait fails.

Anti-Pattern 2: Blanket timeout increases

TypeScript
// ❌ BAD: Increasing all timeouts to hide a specific problem
export default defineConfig({
  timeout: 300_000,  // 5 minutes per test!
  expect: { timeout: 60_000 },  // 60s per assertion!
  use: { actionTimeout: 60_000, navigationTimeout: 120_000 },
});

// ✅ GOOD: Increase only the specific timeout that needs it
export default defineConfig({
  timeout: 60_000,  // Reasonable test timeout
  expect: { timeout: 10_000 },
  use: { actionTimeout: 10_000, navigationTimeout: 30_000 },
});

Blanket increases mask real issues. If one test needs 2 minutes, use test.setTimeout(120_000) in that specific test rather than giving every test a 5-minute budget.

Anti-Pattern 3: Ignoring the root cause

TypeScript
// ❌ BAD: Retrying without understanding why it fails
export default defineConfig({
  retries: 5,  // Just keep retrying until it passes
});

// ✅ GOOD: Use retries for genuine flakiness, not as a fix
export default defineConfig({
  retries: process.env.CI ? 1 : 0,  // 1 retry in CI, 0 locally
  use: { trace: 'on-first-retry' },  // Capture trace to debug the flake
});

Anti-Pattern 4: Using waitForSelector when an assertion works

TypeScript
// ❌ UNNECESSARY: Manual wait + assertion
await page.waitForSelector('.success-message');
const text = await page.textContent('.success-message');
expect(text).toBe('Order placed successfully');

// ✅ BETTER: Web-first assertion handles the wait automatically
await expect(page.getByText('Order placed successfully')).toBeVisible();

Playwright's web-first assertions (expect(locator).toBeVisible(), toHaveText(), etc.) already auto-retry. Adding a manual waitForSelector before them is redundant and makes the code harder to read.


Frequently Asked Questions

How do I fix "Navigation timeout of 30000ms exceeded" in Playwright?

This means page.goto() did not complete within 30 seconds. Fix it by: increasing the timeout with page.goto(url, { timeout: 60000 }), using a less strict waitUntil option like 'domcontentloaded' or 'commit', or setting navigationTimeout globally in your config. Also investigate whether the page genuinely loads slowly due to heavy assets or slow APIs.

What is the default timeout in Playwright?

Playwright has four defaults: test timeout is 30 seconds, action timeout is 0 (no limit), navigation timeout is 30 seconds, and expect timeout is 5 seconds. All four can be configured independently in playwright.config.ts using timeout, use.actionTimeout, use.navigationTimeout, and expect.timeout.

How do I increase the timeout for a single Playwright test?

Use test.setTimeout(120_000) inside the test body for a precise value, or test.slow() to triple the default timeout. You can also set timeout per-project in playwright.config.ts to adjust all tests in that project.

Should I use page.waitForTimeout() in Playwright tests?

No. page.waitForTimeout() is a hard sleep that makes tests slow and flaky. Use event-driven waits instead: page.waitForSelector(), page.waitForLoadState(), page.waitForResponse(), or auto-retrying assertions like expect(locator).toBeVisible(). These resolve as soon as the condition is met rather than waiting a fixed duration.

How do I debug Playwright timeout errors?

Use the Trace Viewer (trace: 'on-first-retry' in config, then npx playwright show-trace) to see a timeline of actions and DOM snapshots at the moment of failure. Run npx playwright test --debug for step-by-step debugging with the Playwright Inspector. Insert page.pause() in your test code to pause at a specific point and inspect the page interactively.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn