ScreenshotNeo

BlogHow-to

Playwright Screenshot Timeout on Slow Pages: How to Fix It

Find which Playwright timeout expired, wait for the page state your screenshot needs, and fix slow-page captures without relying on flaky sleeps.

By the ScreenshotNeo team4 October 20269 min read

A Playwright screenshot timeout on a slow page usually means one of several independent waits expired: the test, a navigation, an assertion, an action, a direct screenshot, or a screenshot assertion. First identify the operation named in the error or call log. Then wait for the page state the image actually needs and increase only the timeout for that operation if the slow work is expected.

For navigation, set an appropriate page.goto() timeout and choose a waitUntil condition. For content that appears after navigation, wait for a locator or web assertion. Avoid treating networkidle or a fixed sleep as a general readiness signal. [Playwright Page API]

1. Identify which timeout expired

Read the error message and call log before editing configuration. A larger test timeout does not necessarily change a navigation, assertion, action, or screenshot timeout. Playwright Test documents a 30,000 ms default test timeout and a 5,000 ms default expect timeout; its timeout table lists no default limit for action and navigation timeouts. These are from Playwright’s undated live timeout documentation, accessed 2026-10-03. [Playwright Test timeouts]

Scope What is waiting Where to inspect or configure
Test The test function, fixture setup, or beforeEach timeout in Playwright Test config or test.setTimeout()
Expect A web assertion or screenshot assertion expect.timeout globally or timeout for one assertion
Navigation A navigation call waiting for a selected browser event page.goto() timeout or use.navigationTimeout
Action An action waiting for its target and actionability conditions Action timeout or use.actionTimeout
Direct screenshot page.screenshot() and its capture work Screenshot call’s timeout option
Screenshot assertion toHaveScreenshot() waiting for stable consecutive captures and comparison Assertion timeout, test timeout, and visual stability

Navigation options, timeout configuration, and the distinctions above are documented in the timeout guide and Page API. [c001] [c002]

2. Choose a navigation wait that matches the capture

page.goto() supports commit, domcontentloaded, load, and networkidle. These wait for different milestones: a response and document loading start; DOM content loaded; the load event; or network activity reaching the documented idle condition. Waiting for a later milestone can be unnecessary if the screenshot only needs a particular element, and pages with ongoing requests may never reach an idle condition that suits your test.

Playwright explicitly discourages networkidle for tests and recommends web assertions to assess readiness. [Page.goto API] A more reliable approach is often to navigate to a suitable milestone and then assert that the content you intend to capture is visible.

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

test('capture the report when its content is ready', async ({ page }) => {
  await page.goto('https://example.com/report', {
    waitUntil: 'domcontentloaded',
    timeout: 60_000,
  });

  const report = page.locator('[data-testid="report"]');
  await expect(report).toBeVisible({ timeout: 30_000 });
  await report.screenshot({ path: 'report.png', timeout: 30_000 });
});

Replace the example URL and selector with your target page and a stable readiness signal. This example sets navigation, assertion, and direct screenshot timeouts separately; adjust them to match the operation’s actual duration and the configured test budget.

3. Configure only the timeout you need

Use per-call timeouts when one destination is unusually slow. Use project or test configuration when a class of pages consistently needs a different budget. Avoid increasing every timeout before identifying the bottleneck.

Per-navigation timeout

await page.goto('https://example.com/report', {
  waitUntil: 'domcontentloaded',
  timeout: 60_000,
});

Playwright Test configuration

import { defineConfig } from '@playwright/test';

export default defineConfig({
  timeout: 60_000, // per-test budget
  expect: {
    timeout: 10_000, // default assertion budget
  },
  use: {
    navigationTimeout: 45_000,
    actionTimeout: 15_000,
  },
});

A test timeout covers time spent in the test function, fixture setup, and beforeEach hooks. A test can still exceed a narrower operation timeout first. Choose budgets with that nesting in mind and leave room for the assertion and capture after navigation. [Playwright Test timeouts]

Per-test and per-assertion overrides

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

test('slow report', async ({ page }) => {
  test.setTimeout(90_000);
  await page.goto('https://example.com/report', { timeout: 60_000 });

  await expect(page.locator('[data-testid="report"]')).toBeVisible({
    timeout: 20_000,
  });
});

4. Wait for the content, not an arbitrary duration

Slow pages often navigate successfully before their important content is ready. Wait for a specific locator or assertion that represents the capture target. Playwright’s actions and assertions provide state-based waiting; a fixed page.waitForTimeout() makes the run depend on an arbitrary delay and can still be too short or unnecessarily long. Playwright’s documentation describes timer-based production tests as flaky. [Page.waitForTimeout API] [Auto-waiting and actionability]

await page.goto('https://example.com/dashboard', {
  waitUntil: 'commit',
  timeout: 45_000,
});

const chart = page.locator('#revenue-chart');
await chart.waitFor({ state: 'visible', timeout: 30_000 });
await page.screenshot({ path: 'dashboard.png', fullPage: true });

Use a selector that signals the content is ready, not merely that a shell or loading placeholder is visible. If the page exposes a clear state such as a completed-results container, assert that state. For an action such as clicking a control before capture, Playwright auto-waits for the action’s required actionability checks; if the action times out, inspect whether the target is present, visible, enabled, and able to receive events. [Playwright actionability]

5. Diagnose direct screenshots and screenshot assertions

Direct page.screenshot()

If the timeout names screenshot, inspect the screenshot call’s own timeout and options. A direct capture can set a longer timeout for a genuinely expensive page capture. The Page API also documents options such as full-page capture and stylesheets for altering or hiding dynamic elements during capture. [Page.screenshot API]

await page.screenshot({
  path: 'full-page.png',
  fullPage: true,
  timeout: 45_000,
  animations: 'disabled',
  style: 'video, .live-clock { visibility: hidden !important; }',
});

Use style only to control visual content that should not appear in the captured image, such as a changing clock. Avoid hiding content that the screenshot is supposed to validate.

toHaveScreenshot()

A screenshot assertion is not just a call to write an image. expect(page).toHaveScreenshot() waits for two consecutive screenshots to be identical before comparing against the expected image, so animations, rotating banners, clocks, carousels, and other changing content can delay or destabilize the assertion. Screenshot assertions work with the Playwright test runner. [Visual comparisons]

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

test('stable report snapshot', async ({ page }) => {
  await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
  await expect(page.locator('[data-testid="report"]')).toBeVisible();
  await expect(page).toHaveScreenshot('report.png', {
    animations: 'disabled',
    timeout: 30_000,
  });
});

First identify content that changes between frames and decide whether to disable its animation, mask it, or remove it from the capture. Raising the test timeout may give the assertion more time, but it does not make a constantly changing page stable.

6. Runnable example with diagnostics

This standalone Playwright Test example records separate milestones so a failure is easier to locate. Install Playwright Test in your project and run the test with your usual Playwright Test command.

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

test('capture a slow page after its report is ready', async ({ page }) => {
  test.setTimeout(90_000);

  const started = Date.now();
  const response = await page.goto('https://example.com/report', {
    waitUntil: 'domcontentloaded',
    timeout: 60_000,
  });
  console.log('navigation ms:', Date.now() - started);
  console.log('HTTP status:', response?.status());

  const report = page.locator('[data-testid="report"]');
  const contentStarted = Date.now();
  await expect(report).toBeVisible({ timeout: 20_000 });
  console.log('report ready ms:', Date.now() - contentStarted);

  const captureStarted = Date.now();
  await report.screenshot({ path: 'report.png', timeout: 20_000 });
  console.log('capture ms:', Date.now() - captureStarted);
});

The timings separate navigation, readiness, and capture. If navigation consumes its full budget, investigate the navigation milestone and response. If navigation completes but the locator assertion expires, check the selector and the page’s client-side data load. If only capture expires, inspect capture size, full-page layout, screenshot options, and the direct screenshot timeout.

7. Troubleshooting common timeout patterns

Symptom Likely cause Fix
page.goto times out waiting for load Late images, scripts, or other resources delay the load event Choose a sufficient earlier milestone such as domcontentloaded, then wait for the specific capture target
page.goto times out waiting for networkidle The page keeps making requests or the idle condition is unsuitable for its behavior Use a different navigation milestone and assert the required page state; Playwright discourages network idle for tests
Navigation passes, content assertion times out Wrong selector, slow client-side request, failed page data, or a state that never occurs Check the selector and page state; inspect the call log and response status; wait for a meaningful success state
Click or other action times out Target is not actionable, is covered, or never appears Inspect locator, visibility, enabled state, overlays, and actionability checks
Direct screenshot times out Capture work exceeds screenshot timeout or capture involves a large/complex page Inspect screenshot options and target size; increase the screenshot timeout if the capture legitimately needs longer
toHaveScreenshot times out or remains unstable Visual content changes between consecutive captures Disable or mask expected dynamic content, and give the assertion an appropriate budget
Test timeout occurs despite larger navigation timeout The whole test budget includes setup, hooks, assertions, and screenshot work Set a suitable test budget as well as the narrow operation timeout, then identify which phase consumes it
Increasing timeout has no effect A different timeout scope is expiring first Use the named operation in the error/call log to configure the correct scope

8. Performance, reliability, and cost considerations

  • Wait only as long as the capture requires. A specific readiness locator can avoid waiting for unrelated requests or late resources.
  • Keep timeout budgets proportional. Longer timeouts make slow cases recoverable but also make a truly stuck run take longer to fail. Set an outer test budget that accommodates the expected navigation, readiness assertion, and capture.
  • Make visual output stable. For screenshot assertions, control animations and known dynamic regions so a changing page does not spend time waiting for identical frames.
  • Measure each phase. Log navigation, readiness, and capture durations separately. This reveals whether the fix belongs in navigation conditions, page-state waiting, or capture options.
  • Account for full-page work. Full-page captures can involve substantially more page area than a viewport capture; use an element screenshot when that is all the test needs.
  • Budget test-runner time and compute. Increasing waits consumes runner time and can slow a suite. No universal timeout value fits every site, browser, or CI environment; derive one from the operation and environment you run.

9. Or skip the browser setup

If your goal is to capture a slow page rather than exercise Playwright itself, ScreenshotNeo provides a website screenshot API and MCP server. Its one-call API can return a screenshot without configuring a browser in your project. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Replace YOUR_API_KEY with your key and change the target URL. ScreenshotNeo accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.

10. Frequently asked questions

Does raising the test timeout also raise the navigation timeout?

No. Test, navigation, assertion, action, and screenshot waits are separate scopes. Configure the scope named by the error.

When should I use waitUntil: 'load'?

Use it when the capture depends on resources associated with the load event. If the target content is ready earlier or is controlled by client-side data, navigate to an appropriate earlier milestone and wait for that content explicitly.

Why does a screenshot assertion take longer than writing a screenshot file?

toHaveScreenshot() waits for two consecutive identical captures before comparison. A direct screenshot does not have that same visual-stability assertion behavior.

Are the documented timeout defaults guaranteed for every installed Playwright version?

Check the documentation matching your installed version and project configuration. The cited values come from Playwright’s live, undated documentation accessed 2026-10-03.

Sources