← All posts

Batch Screenshot API: Full-Site Design QA at Scale

September 19, 2026

Why Single-Page Screenshots Don't Scale for Design QA

Screenshotting a page one at a time works fine for a single template. It falls apart when you need design QA screenshots at scale — after a redesign, a CMS migration, or a shared component change touching every template. Manual screenshotting is tedious at 50 pages; at 500 or 5,000, it's not a task a human should do — you'll miss pages, forget viewport sizes, and lose track of what changed between staging and production.

Full-site screenshot automation treats "capture the whole site" as a pipeline: discover every URL, render them all, handle failures, and land the images somewhere a reviewer can scan quickly. The rest of this article walks through that pipeline with a runnable pattern, using a batch screenshot API instead of a browser you drive by hand.

Step 1: Build the URL List From Your Sitemap

You can't capture every page if you don't know what every page is. The most reliable source is sitemap.xml — most frameworks and CMSs generate one automatically, usually more complete than a hand-maintained route list.

A basic sitemap screenshot crawler just needs to fetch the XML and pull out values:

import { XMLParser } from "fast-xml-parser";

async function getUrlsFromSitemap(sitemapUrl) {
  const xml = await fetch(sitemapUrl).then(r => r.text());
  const parsed = new XMLParser().parse(xml);

  // Sitemap index: recurse into child sitemaps
  if (parsed.sitemapindex) {
    const children = [].concat(parsed.sitemapindex.sitemap).map(s => s.loc);
    const nested = await Promise.all(children.map(getUrlsFromSitemap));
    return nested.flat();
  }

  // Regular urlset
  return [].concat(parsed.urlset.url).map(u => u.loc);
}

Sitemap index files (a sitemap listing other sitemaps) are common on larger sites, so handle that case explicitly rather than assuming a flat urlset. If a site has no sitemap, fall back to a shallow crawl following internal links from the homepage — slower, but it still gets a complete URL list before you spend any screenshot budget.

Step 2: Fan Out Requests With Bounded Concurrency

Once you have a few hundred URLs, firing them all at once is the fastest way to get rate-limited or melt your own network stack. You want concurrent screenshot requests capped at a sane limit, so the batch screenshot API processes a steady stream instead of a spike.

Here's a Node.js pattern using a simple concurrency-limited queue:

import pLimit from "p-limit";

const limit = pLimit(8); // 8 concurrent renders

async function captureAll(urls, apiKey) {
  const results = await Promise.allSettled(
    urls.map(url =>
      limit(() => captureOne(url, apiKey))
    )
  );
  return results;
}

async function captureOne(url, apiKey) {
  const res = await fetch("https://api.browsevra.com/v1/screenshot", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      url,
      fullPage: true,
      viewport: { width: 1440, height: 900 }
    })
  });

  if (!res.ok) throw new Error(`${url} → ${res.status}`);
  return { url, buffer: await res.arrayBuffer() };
}

The exact request body and response shape depend on the endpoint version — check the Docs for current parameters like device emulation, delay-before-capture, and cookie injection for staging environments behind auth. The concurrency number (8 above) is a starting point; raise it gradually while watching your account's rate limits rather than guessing.

Step 3: Handle Failures Without Killing the Whole Run

At batch scale, some pages will fail — a 404 that slipped past the sitemap, a redirect loop, a slow client-side render that times out. None of that should abort bulk website screenshots for QA; a single bad URL shouldn't cost you the other 4,999 results.

Promise.allSettled (used above) already gives you this for free — it resolves every promise, successful or not, instead of short-circuiting on the first rejection like Promise.all would. Pair it with a small retry wrapper for transient failures:

async function withRetry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
      await new Promise(r => setTimeout(r, 500 * (i + 1)));
    }
  }
}

Wrap captureOne in withRetry and log failures to a separate file or table (url, error, timestamp) rather than mixing them into the success output. That failure log is its own useful QA artifact — recurring failures on the same route often point to a real bug, not a flaky network blip.

Step 4: Organize Output for Fast QA Review

A folder of 3,000 files named screenshot-1.png through screenshot-3000.png is useless. Name files after the route so a reviewer can scan them in order and immediately recognize what they're looking at:

output/
  home.png
  about.png
  blog/
    2024-hiring.png
    2024-launch.png
  products/
    widget-a.png
    widget-b.png

Derive the path directly from the URL (strip the domain, replace / with directory separators, default to home for the root). If checking responsive layouts, capture each page at multiple viewports and group by device rather than interleaving them — a desktop/ and mobile/ split is easier to scan than alternating sizes in one flat folder.

Decide upfront whether you need full-page screenshot automation (the entire scrollable page) or just the visible viewport. Full-page capture is better for catching layout breaks further down a page, but takes slightly longer to render and produces larger files — worth knowing before pointing the job at thousands of URLs. For storage, dumping images straight to S3 or another cloud bucket, keyed by run date and route, makes it easy to diff one run against the last — the workflow covered in Visual Regression Testing Screenshot API: A CI/CD Pattern if you want to turn this into an automated regression check rather than a one-off QA pass.

What Batch Screenshots Cost at Scale

Screenshot API cost scales roughly linearly with two numbers: pages per run, and runs per month. A 200-page site screenshotted once after every deploy costs a fraction of a 5,000-page catalog re-screenshotted nightly for visual regression monitoring. Before running a full-site job, estimate pages × runs per period × per-screenshot price and check that against your plan — see Pricing for current rates and volume tiers.

Two levers cut cost meaningfully at scale: capturing only routes that actually changed (diff your sitemap against the last run instead of re-shooting everything), and speeding up individual renders — blocking unnecessary resources like fonts and analytics scripts shortens page load per screenshot, which matters when rendering thousands of them. That technique is covered in Headless Browser Block Resources: Faster Screenshots & PDFs.

Frequently Asked Questions

How many pages can I screenshot at once with a batch screenshot API?

There's no hard cap on total pages in a batch — the real constraint is your concurrency setting and account rate limits. Most teams process hundreds to thousands of pages per run by keeping concurrency modest (5–15 simultaneous requests) and letting the queue work through the full list rather than firing everything at once.

What's the best way to get a full list of URLs before running a batch screenshot job?

Parse sitemap.xml first — it's the most complete and maintained source of URLs on most sites, and handles sitemap index files by recursing into each child sitemap. If no sitemap exists, fall back to a link-following crawl starting from the homepage.

How do I avoid rate limits or timeouts when screenshotting hundreds of pages?

Cap concurrency with a limiter like p-limit instead of firing all requests simultaneously, and wrap each request in a retry with backoff for transient timeouts. Increase concurrency gradually while monitoring response codes rather than assuming a high number is safe from the start.

Should batch screenshots be full-page or viewport-only for design QA?

Full-page capture is generally better for design QA because it catches layout breaks that only appear further down a scrollable page. Viewport-only capture is faster and produces smaller files, a reasonable choice when you only care about above-the-fold content or are running very frequent checks.

How do I handle pages that fail to render during a batch run?

Use Promise.allSettled instead of Promise.all so one failed page doesn't cancel the rest of the batch, then log failures separately with the URL, error, and timestamp. Retry transient failures (timeouts, temporary 5xx errors) a few times before giving up and flagging the page for manual review.

Can I run batch screenshots on a schedule for ongoing design QA?

Yes — the same sitemap-to-batch-capture pattern can run on a cron job or CI pipeline trigger, comparing each run's output against the previous one. This turns one-off QA screenshots into an ongoing visual regression check, the approach detailed in the CI/CD screenshot pattern linked above.

Run this pattern against a real site with a Browsevra API key — the Docs cover every parameter used in the code above, and Pricing will tell you exactly what a full-site run costs before you kick it off. Signing up with browsevra is the fastest way to point this script at your own sitemap and see the batch screenshot API in action.