How to Run BackstopJS Tests in Parallel
BackstopJS already captures and compares screenshots in parallel. Learn how to tune each concurrency limit, keep memory in check, and wire results into CI.
BackstopJS already runs screenshot captures and image comparisons in parallel. Set the root-level asyncCaptureLimit and asyncCompareLimit options in your configuration to control those two stages independently. The project README lists defaults of 10 concurrent captures and 50 concurrent comparisons; verify these values against the BackstopJS release installed in your project because the README is mutable. Increasing concurrency can use more RAM.
1. Configure capture and comparison concurrency
Start with the BackstopJS configuration your project already uses. The values below are an illustrative starting point, not universal recommendations:
{
"asyncCaptureLimit": 5,
"asyncCompareLimit": 20
}
asyncCaptureLimit controls how many screenshot captures BackstopJS runs concurrently. asyncCompareLimit controls concurrent image comparisons. They affect different stages, so tune them separately. Lower limits can reduce simultaneous work and ease memory pressure; higher limits may improve throughput when the runner has capacity.
| Setting | Controls | Documented README default |
|---|---|---|
asyncCaptureLimit |
Concurrent screenshot captures | 10 |
asyncCompareLimit |
Concurrent image comparisons | 50 |
Defaults and configuration behavior can vary by release. Check the README or configuration shipped with the exact version in your lockfile before relying on a version-specific value. The project gives only an approximate memory rule of thumb: about 100 MB baseline plus roughly 5 MB per concurrent comparison. This is not a guarantee or a safe-memory calculator; browser processes, image dimensions, scenarios, and the runner all affect actual use.
2. Run the test
Local CLI
Install BackstopJS in the project and invoke its local executable. Omit --config when using the default backstop.json; pass a path when the configuration has another name or location.
./node_modules/.bin/backstop test
./node_modules/.bin/backstop test --config=backstop.json
npm script
Add a project script so developers and CI use the same command:
{
"scripts": {
"visual:test": "backstop test --config=backstop.json"
}
}
npm run visual:test
Node API
BackstopJS also documents a Node API for build orchestration. With BackstopJS installed as a project dependency, a CommonJS script can invoke its test operation:
const backstop = require('backstopjs');
backstop('test', { config: 'backstop.json' })
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
Use the API signature supported by your installed BackstopJS release. The CLI is the simplest choice when an existing build does not need programmatic orchestration.
3. Tune parallel work without exhausting memory
- Run a representative test suite at the current limits and observe the runner’s peak memory and duration.
- If captures are the bottleneck and memory is available, raise
asyncCaptureLimita little. If comparisons dominate, adjustasyncCompareLimitseparately. - Repeat with the same scenarios and runner conditions. Keep a setting only if it improves the workload without causing memory pressure or instability.
- If the process runs out of memory or becomes unreliable, reduce the limit associated with the busy stage and rerun.
There is no universal best value: the right limits depend on screenshot sizes, browser overhead, available runner memory, and workload. BackstopJS documents its internal capture and comparison limits. Splitting scenarios among independent CI workers is an orchestration choice; the reviewed documentation does not establish built-in sharding semantics. You can use the documented --filter option to select matching scenario names while debugging:
./node_modules/.bin/backstop test --filter="checkout"
Confirm the filter syntax against your installed release and scenario naming. A filter is useful for focused runs; it does not itself distribute work across machines.
4. Make CI results actionable
The BackstopJS README documents CI reporting that generates JUnit output. Configure it as described for your installed version, then have the CI system collect the report as a test artifact or report. The CLI exits with status 0 on success and 1 when anything fails, so a normal pipeline step can gate the build on visual regressions.
./node_modules/.bin/backstop test --config=backstop.json
Keep the command’s exit status intact in any wrapper script; a wrapper that always exits successfully can hide a failed visual test. Ensure the CI job retains the generated JUnit report even when the test command fails, according to that CI system’s artifact settings.
5. Keep rendering environments consistent
Text and other page content can render differently across environments. The BackstopJS README documents backstop test --docker as an option for running in Docker, which can help standardize the rendering environment:
backstop test --docker
The published BackstopJS Docker image documents mounting the working directory at /src. Its listing also notes that backstop openReport is unsupported in that image. Check the image documentation and the version you use before building a workflow around report opening or volume paths.
6. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Memory use spikes or the process is killed | Too many captures or comparisons are active for the runner’s available memory. | Lower the relevant concurrency limit. Tune capture and compare limits independently, then observe a representative run; the documented memory estimate is approximate. |
| Changing a limit has no effect | The setting may be in the wrong place, the command may load a different config, or the installed release may handle configuration differently. | Put the options at the root of the active configuration, pass the intended path with --config, and check the documentation for the installed release. |
| Local and CI screenshots differ | Rendering environments can produce different text or page output. | Use a consistent environment; the project documents its Docker option. Confirm that local and CI use compatible configuration and browser setup. |
| A filtered run still includes unexpected scenarios | The filter may not match the scenario names as expected, or syntax may differ in the installed version. | Check scenario names and the installed CLI’s documented --filter behavior, then run the focused subset again. |
| The pipeline passes despite visual failures | A shell wrapper or CI step may be discarding BackstopJS’s nonzero exit code. | Run the command directly or make the wrapper propagate its exit status. Collect the documented JUnit output for diagnosis. |
openReport is unavailable in the container |
The published BackstopJS Docker image lists backstop openReport as unsupported. |
Use the report workflow supported by your environment or open the report outside that image. |
7. Capture a page without managing a browser
If you need standalone website screenshots alongside your visual regression suite, ScreenshotNeo is a website screenshot API and MCP server for developers. It is separate from BackstopJS and does not replace BackstopJS’s baseline comparison workflow.
Or skip the browser setup
Make one GET request with the target URL. The response is an image or PDF; see the ScreenshotNeo API documentation for options and response details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a 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.
FAQ
Does BackstopJS already run tests in parallel?
Yes. Its documentation says screenshot capture and image comparison are processed in parallel, with separate configuration limits for each stage.
Should the comparison limit always be higher than the capture limit?
The documented defaults differ, but there is no universal ratio. Choose each limit based on the workload and runner capacity.
Can I use concurrency settings to split a suite across CI machines?
The settings tune concurrency within BackstopJS. The reviewed documentation does not establish a built-in cross-worker sharding feature; distributing work requires orchestration decisions outside those limits.


