ScreenshotNeo

BlogHow-to

How to Fix BackstopJS Timeout Errors on Slow Pages

Diagnose whether a BackstopJS timeout comes from navigation or page readiness, then choose the right fix for slow pages, CI, and Docker.

By the ScreenshotNeo team4 October 20267 min read

To fix a BackstopJS timeout on a slow page, first identify whether the failure occurs during browser navigation or while BackstopJS waits for the page’s readiness condition. For content that renders after navigation, use a reliable readySelector or app-emitted readyEvent; raise readyTimeout only if that valid condition eventually occurs. A navigation timeout needs investigation of URL reachability and browser navigation behavior instead.

BackstopJS documents a default readyTimeout of 30,000 ms for readyEvent and readySelector. Check the version installed in your project, since package and engine behavior can change. BackstopJS package documentation · BackstopJS project documentation

1. Identify which timeout is occurring

Read the full error and determine which phase failed before changing configuration:

Failure phase What BackstopJS is waiting for First place to investigate
Navigation The browser navigating to the scenario URL under its configured navigation behavior URL reachability, redirects, authentication, browser errors, and engine navigation options
Readiness The configured readySelector or readyEvent after navigation Whether the condition is correct, appears in this scenario, and waits for the required app work

These phases need different fixes. Increasing readyTimeout affects readiness checks; it does not repair an unreachable URL or a navigation that never completes.

2. Reproduce one failing scenario

  1. Copy the complete error message and note whether it names navigation, readySelector, or readyEvent.
  2. Run only the failing scenario using --filter and its label pattern, for example backstop test --filter="Product detail". Use the command and label syntax supported by your installed BackstopJS version.
  3. Compare the same scenario in the environment where it fails (local machine, CI, or Docker). Record the URL, redirects, and whether the content needed for the screenshot appears.

Filtering narrows the run while keeping the scenario intact, which makes it easier to distinguish an individual page problem from suite-wide resource pressure.

3. Wait for the content that matters

For an app that progressively renders after navigation, set readySelector to a DOM element that appears only when the specific content required by the screenshot is ready. Verify the selector against the rendered page and avoid a generic element that exists before the data arrives.

{
  "scenarios": [
    {
      "label": "Product detail",
      "url": "https://example.com/products/42",
      "readySelector": "#product-details-loaded",
      "readyTimeout": 60000
    }
  ]
}

This is a configuration example; replace the URL and selector with ones from your application. The 60-second bound is illustrative, not a universal recommendation. Start with the shortest bound that covers the observed legitimate load time. The documented default for readiness is 30,000 ms.

Use an application readiness event when the app owns readiness

If the application can reliably signal when its required data and UI dependencies are finished, configure readyEvent and emit the matching console message from the app at that point:

{
  "scenarios": [
    {
      "label": "Dashboard",
      "url": "https://example.com/dashboard",
      "readyEvent": "backstopjs_ready",
      "readyTimeout": 60000
    }
  ]
}

The application is responsible for waiting for the dependencies relevant to the screenshot before emitting the event. Do not emit it merely because the initial HTML or loading shell has appeared. Confirm the event spelling and case match exactly.

Use delay only for a known settling period

delay is a fixed wait in milliseconds. It can cover a predictable animation or brief settling period after readiness. When combined with readyEvent, BackstopJS applies the delay after the event.

{
  "scenarios": [
    {
      "label": "Animated chart",
      "url": "https://example.com/analytics",
      "readySelector": "[data-chart-ready='true']",
      "delay": 500
    }
  ]
}

Use a page-specific selector or event to represent variable app work. A fixed delay alone can be too short on a slow run and unnecessarily long on a fast one.

4. Adjust navigation behavior only for a navigation timeout

BackstopJS documents gotoParameters as an engine option and gives networkidle0 as an example:

{
  "engineOptions": {
    "gotoParameters": {
      "waitUntil": "networkidle0"
    }
  }
}

Treat this as an example, not a universally correct setting. A page with polling, streaming, or long-lived requests may not become network-idle. Select navigation behavior that matches the application and the browser engine version used by your BackstopJS installation. Changing navigation behavior does not replace a readiness condition for content rendered after navigation.

5. Check runtime, network, and suite pressure

  • URL reachability: Open the scenario URL from the same machine or container running BackstopJS. Check DNS, network access, redirects, login requirements, and certificates.
  • Browser errors: Inspect browser console and network failures for blocked scripts, failed API calls, or resources that prevent the app from reaching its ready state.
  • Docker networking: A scenario using localhost inside Docker may refer to the container itself, not the host. The BackstopJS documentation describes host.docker.internal as an alternative for Mac and Windows setups; verify the correct host address for your environment.
  • Concurrency: If many captures run at once and the environment appears overloaded, lower asyncCaptureLimit. It controls concurrent work; it does not extend a timeout or signal that a page is ready.
  • Version alignment: Check the locked BackstopJS and browser-engine versions in local and CI environments. Engine options and defaults can vary, so compare the actual installed versions before copying configuration from another release.

6. Troubleshooting common errors

Symptom Likely cause Fix
Readiness timeout for a selector The selector is misspelled, absent on this route, or appears before the required content is ready. Inspect the rendered DOM, correct the selector, and choose an element that represents the screenshot’s required state. Raise readyTimeout only if the valid element appears eventually.
Readiness timeout for an event The app never emits the configured console string, emits a different string, or emits it too early in another code path. Make the app emit the exact event after required work completes; check spelling, case, and route-specific execution.
Navigation timeout, while readiness settings look correct The URL cannot be reached, redirects or authentication stall, browser navigation fails, or the navigation condition does not fit the page. Test reachability in the runner, inspect redirects and browser errors, then review engine navigation options. Do not expect a larger readyTimeout to fix navigation.
Failure only in Docker or CI Different network access, hostnames, environment variables, browser launch setup, or browser versions. Compare the runner environment with local; verify the URL is reachable from the container and check the documented Docker host configuration for your platform.
Intermittent failures across many scenarios Resource pressure or unstable dependencies may be delaying multiple captures. Look for shared browser, CPU, memory, or network pressure. Reduce asyncCaptureLimit if concurrent captures overwhelm the runner; investigate shared application dependencies separately.
Capture succeeds but shows a loading shell The page is considered navigated, but the app has not reached the state needed for the screenshot. Add a meaningful readySelector or app-controlled readyEvent; a longer navigation wait may not address post-navigation rendering.

7. Performance and reliability guidance

  • Prefer an explicit selector or event over a large fixed delay when rendering time varies. It lets the capture proceed once the condition is met.
  • Keep readiness conditions tied to the content under test. A selector that is always present can make a capture deterministic but too early.
  • Set a finite timeout based on observed legitimate load behavior. A larger bound gives a slow but valid page more time, while also making a genuinely stuck scenario take longer to fail.
  • Use delay for a measured, predictable post-readiness effect, such as a short animation settle period.
  • Reduce capture concurrency only when there is evidence of runner resource pressure. This can ease load but may increase total suite time.
  • Keep local, CI, and container browser and BackstopJS versions aligned where possible, and verify navigation settings against the installed engine.

The cited documentation establishes option behavior and examples; it does not provide a universal timeout value or performance benchmark. No single wait condition fits every application.

8. Or skip the browser setup

If the goal is to capture a page rather than run a visual regression suite, ScreenshotNeo provides a website screenshot API and MCP server. It returns a screenshot or PDF from one GET request. The full option list and request details are in the ScreenshotNeo API documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.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. The Node.js example uses Bun’s file-writing helper; with Node.js, save the response bytes using fs. Check the response headers and body handling described in the docs before treating every response as an image.

  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Responses report the page verdict and billing status in headers.
  • An MCP server lets AI agents, including Claude and Cursor, take screenshots with tools for screenshots, page information, and PDF capture.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

Frequently asked questions

Does increasing readyTimeout fix every BackstopJS timeout?

No. It extends the bound for readySelector and readyEvent. It does not fix a navigation failure, a selector that never appears, or an event that is never emitted.

Should I use readySelector or readyEvent?

Use a selector when a DOM element reliably marks the needed rendered state. Use an event when the application can signal readiness after its relevant data and UI dependencies are complete.

Does delay run before or after readyEvent?

When both are configured, BackstopJS applies delay after the ready event.

What is the default readyTimeout?

The npm package documentation lists 30,000 ms. Confirm the documentation for the version installed in your project.

Will asyncCaptureLimit make a slow page ready sooner?

No. It controls capture concurrency. It may help when simultaneous captures overwhelm the runner, but it does not change page readiness.

References