How to Capture Screenshots of Multiple URLs with Browserless in Parallel
Capture multiple URLs with Browserless by sending one screenshot request per page and controlling concurrency. Save each image safely and handle slow or blocked pages.
To capture screenshots of multiple URLs with Browserless in parallel, send one POST /screenshot request for each URL and run those requests concurrently. Each response is image bytes, so save it to a distinct filename. Keep the number of simultaneous requests within the concurrency supported by your Browserless plan. Each REST call opens an independent browser session. Browserless concurrent sessions documentation
The examples below use the regional endpoint https://production-sfo.browserless.io/screenshot. Use the endpoint appropriate for your account or region. You will need a Browserless API token. The request body contains a url and an options object; the response is a PNG, JPEG, or WebP file, depending on the requested type. Browserless Screenshot API documentation
1. Choose capture settings
Every URL can use the same options or its own settings. Common settings include:
| Need | Setting or approach |
|---|---|
| Capture the visible viewport | Omit fullPage or set it to false. |
| Capture the whole document | Set options.fullPage to true. |
| Choose output format | Set options.type to png, jpeg, or webp; match the filename extension. |
| Control page dimensions | Set viewport dimensions and device scale factor using supported screenshot settings. |
| Capture one element | Set selector at the top request level, alongside url, not inside options. |
| Capture a fixed rectangle | Use options.clip with coordinates and dimensions. |
| Wait for a page to be ready | Configure waits for events, selectors, functions, or timeouts; navigation settings are configurable with gotoOptions. |
| Load lazy content | Set top-level scrollPage: true; combine with fullPage: true for a long page. |
| Skip unwanted network requests | Use rejectResourceTypes or rejectRequestPattern where appropriate. |
Browserless documents Puppeteer-style screenshot options, including output type, full-page capture, quality, clip, viewport, device scale factor, and selector capture. A selector capture waits for the element and crops to its bounding box. Check the API reference for the complete current option schema. Screenshot API options
2. Capture URLs with cURL
For a short list, start one cURL process per URL in the background and wait for them. This shell example captures three full-page PNGs. Replace the token and URLs. It uses separate output paths and collects exit codes so a failed request does not appear to have succeeded.
#!/usr/bin/env bash
set -u
TOKEN="YOUR_API_TOKEN_HERE"
ENDPOINT="https://production-sfo.browserless.io/screenshot"
curl --fail-with-body --silent --show-error -X POST \
"$ENDPOINT?token=$TOKEN" \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/","options":{"type":"png","fullPage":true}}' \
--output page-1.png &
pid1=$!
curl --fail-with-body --silent --show-error -X POST \
"$ENDPOINT?token=$TOKEN" \
-H 'Content-Type: application/json' \
-d '{"url":"https://www.iana.org/","options":{"type":"png","fullPage":true}}' \
--output page-2.png &
pid2=$!
curl --fail-with-body --silent --show-error -X POST \
"$ENDPOINT?token=$TOKEN" \
-H 'Content-Type: application/json' \
-d '{"url":"https://www.example.org/","options":{"type":"png","fullPage":true}}' \
--output page-3.png &
pid3=$!
failed=0
wait "$pid1" || failed=1
wait "$pid2" || failed=1
wait "$pid3" || failed=1
if [ "$failed" -ne 0 ]; then
echo "One or more screenshot requests failed" >&2
exit 1
fi
echo "Saved all screenshots"
--fail-with-body makes HTTP errors fail the command while retaining the response body for diagnosis. Avoid printing or sharing the token: it is in the request URL. For a long list, use a bounded worker script or job runner instead of launching every request at once.
3. Capture URLs with Python
This runnable script uses requests and ThreadPoolExecutor to cap the number of simultaneous requests. Install the dependency with python -m pip install requests, set BROWSERLESS_TOKEN, then run the script. It writes each response to a numbered PNG and fails visibly on HTTP errors.
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from urllib.parse import urlparse
import requests
TOKEN = os.environ.get("BROWSERLESS_TOKEN")
if not TOKEN:
raise SystemExit("Set the BROWSERLESS_TOKEN environment variable")
ENDPOINT = "https://production-sfo.browserless.io/screenshot"
URLS = [
"https://example.com/",
"https://www.iana.org/",
"https://www.example.org/",
]
OPTIONS = {"type": "png", "fullPage": True}
MAX_WORKERS = 3 # Set this at or below your supported plan concurrency.
OUT_DIR = Path("screenshots")
OUT_DIR.mkdir(parents=True, exist_ok=True)
def capture(item):
index, page_url = item
response = requests.post(
ENDPOINT,
params={"token": TOKEN},
json={"url": page_url, "options": OPTIONS},
timeout=(15, 120),
)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
if not content_type.startswith("image/"):
raise RuntimeError(
f"Unexpected response for {page_url}: {content_type}; "
f"body starts {response.text[:200]!r}"
)
path = OUT_DIR / f"page-{index:03d}.png"
path.write_bytes(response.content)
return page_url, path, len(response.content)
errors = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
futures = [pool.submit(capture, item) for item in enumerate(URLS, 1)]
for future in as_completed(futures):
try:
url, path, size = future.result()
print(f"Saved {url} -> {path} ({size} bytes)")
except Exception as exc:
errors.append(exc)
print(f"Capture failed: {exc}")
if errors:
raise SystemExit(f"{len(errors)} capture(s) failed")
The connection timeout is 15 seconds and the read timeout is 120 seconds in this example; tune them to your pages and workload. The output extension must match the requested image type. The unused urlparse import is not needed; you can omit that import in your own copy.
4. Capture URLs with Node.js
This Node.js 18+ example uses built-in fetch, bounds parallel work, checks the HTTP status and image content type, and writes unique files. Save as capture.mjs, set BROWSERLESS_TOKEN, and run node capture.mjs.
import { mkdir, writeFile } from 'node:fs/promises';
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set the BROWSERLESS_TOKEN environment variable');
const endpoint = 'https://production-sfo.browserless.io/screenshot';
const urls = [
'https://example.com/',
'https://www.iana.org/',
'https://www.example.org/',
];
const options = { type: 'png', fullPage: true };
const concurrency = 3; // Keep at or below the concurrency supported by your plan.
await mkdir('screenshots', { recursive: true });
async function capture(url, index) {
const response = await fetch(`${endpoint}?token=${encodeURIComponent(token)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, options }),
signal: AbortSignal.timeout(135_000),
});
if (!response.ok) {
const detail = (await response.text()).slice(0, 500);
throw new Error(`${url}: HTTP ${response.status}: ${detail}`);
}
const type = response.headers.get('content-type') ?? '';
if (!type.startsWith('image/')) {
throw new Error(`${url}: expected image bytes, received ${type}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
const path = `screenshots/page-${String(index + 1).padStart(3, '0')}.png`;
await writeFile(path, bytes);
return { url, path, bytes: bytes.length };
}
const results = [];
const failures = [];
let next = 0;
async function worker() {
while (next < urls.length) {
const index = next++;
try {
results[index] = await capture(urls[index], index);
} catch (error) {
failures.push({ url: urls[index], error });
}
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, worker));
for (const result of results.filter(Boolean)) {
console.log(`Saved ${result.url} -> ${result.path} (${result.bytes} bytes)`);
}
for (const failure of failures) {
console.error(`Failed ${failure.url}:`, failure.error);
}
if (failures.length) process.exitCode = 1;
Using Promise.all(urls.map(...)) is concise for a small batch, but it starts every request immediately. The worker pool above avoids that for larger URL lists. Browserless says requests above a plan’s concurrency limit queue automatically; a local cap also avoids creating an unnecessarily large burst. Concurrent session guidance
5. Tune concurrency and page readiness
- Start with a small cap. Set the worker count no higher than the concurrency your account supports. Browserless does not publish one universal limit applicable to every plan in the cited guide; check your account’s plan details.
- Set readiness conditions per site. A fast page may be ready after navigation; a JavaScript-heavy page may need a selector or a wait condition. Avoid relying on a single arbitrary delay for every URL.
- Scroll for lazy content. Use
scrollPage: truewhen content or images load only as the page scrolls, along with full-page capture when you need the whole document. - Keep filenames deterministic and unique. Index-based names avoid collisions even if two input URLs share a hostname or path. For repeatable pipelines, store the source URL alongside each output.
- Inspect representative output. A successful HTTP response only means the endpoint returned an image. The image may show a CAPTCHA, access-denied page, blank viewport, or incomplete content.
- Retry selectively. Retry transient timeouts or server errors with a small bounded retry policy and backoff. Do not retry permanent 4xx responses blindly, and avoid multiplying requests for pages that consistently block automation.
For full-page captures, large documents and unusually tall pages can take longer and produce larger files than viewport captures. Choose JPEG or WebP when supported and suitable for the downstream use; PNG is useful when sharp text or lossless output matters. There is no universal speedup figure: page load time, target-site behavior, image size, and available Browserless concurrency determine total completion time.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| HTTP 401 or 403 from the endpoint | Missing, invalid, or wrong-account token; incorrect regional endpoint. | Check the token and endpoint in your Browserless account. Keep the token out of source control and logs. |
| HTTP error response saved with a .png suffix | The request did not succeed, but output handling kept the image filename. | Check the HTTP status before writing bytes. With cURL, use --fail-with-body; with Python or Node, raise or branch on non-success status. |
| Blank or white screenshot | The page may not have finished rendering, or automation may be blocked. | Add an appropriate wait for a selector or event, inspect the returned image, and check whether the site presents a CAPTCHA or access-denied response. |
| CAPTCHA, 403, or access-denied screen in the image | The target site is blocking automated browsing. | Recognize that the capture reflects the page Browserless received. Follow the target site’s access rules and investigate the documented Browserless options for your use case. |
| Missing images or below-the-fold content | Lazy-loaded elements never entered the viewport. | Set scrollPage: true; pair with fullPage: true when capturing a long page. |
| Missing element or selector timeout | The selector is wrong, conditional, or appears later than the configured wait. | Confirm the selector on the rendered page, allow the relevant wait, and account for pages where the element is optional. |
| Some URLs fail while others finish | One worker rejection can interrupt a naive all-or-nothing flow; target pages also vary in availability and load time. | Handle each future independently, record URL plus error, and decide whether to retry just failed items. |
| Outputs overwrite one another | Multiple URLs were mapped to the same path. | Use a unique index or sanitized URL-derived name; never use an unsanitized full URL as a filesystem path. |
| Requests seem queued or slower at higher parallelism | The requested concurrency exceeds available account capacity, or the target sites are slow. | Lower the local worker cap and compare completion times with smaller batches. Do not assume more workers always improve throughput. |
Browserless documents blank captures, CAPTCHA pages, access-denied/403 pages, and missing elements as symptoms to inspect. Browserless bot-detection troubleshooting
7. Reliability, performance, and cost considerations
- Concurrency is a capacity setting. Each REST call uses an independent session. Your plan determines supported concurrency; excess work can queue. Set a cap that fits the plan and target websites.
- Parallelism reduces waiting only when capacity is available. It does not make an individual page load faster, and launching too many jobs may add queue time or burden the destination sites.
- Use per-URL outcomes. Keep a manifest of input URL, output file, status, and error. This makes reruns targeted and avoids recapturing successful pages unnecessarily.
- Protect credentials. The token is a query parameter for this endpoint. Read it from an environment variable, restrict access to logs, and rotate it if exposed.
- Budget from your account plan and workload. The cited Browserless documentation establishes that a token and a plan supporting the needed concurrency are prerequisites, but does not establish a universal price, plan limit, or per-screenshot cost. Check your account for current terms.
- Be considerate of destination sites. Apply a modest cap, avoid needless repeated captures, and respect site access rules.
Or skip the browser setup
ScreenshotNeo captures screenshots through one GET request and also offers an API for batches. For multiple pages, use its bulk capture capability (up to 100 URLs per call); for a single capture, the API call is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Cookie and consent banners are accepted like a visitor and removed, along with supported 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 never billed, and response headers report the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.
FAQ
Does each URL need a separate Browserless request?
Yes. The REST pattern is one POST per page; run those independent requests concurrently when you need parallel captures.
Does the screenshot endpoint return JSON?
No. A successful screenshot response contains image bytes. Read the response as bytes and save it with the matching format extension.
Will parallel requests always finish faster?
No. The result depends on page load times, account concurrency, queueing, and target-site behavior. Use a bounded pool and measure your own workload.
Can the URLs use different settings?
Yes. Build the request body for each URL separately, including its own screenshot options and readiness settings.
Can I capture a page that requires a login?
The basic examples do not configure authentication. Browserless documents saved profiles for pre-authenticated sessions in its concurrent-session guidance; use the appropriate supported setup for your account and protect any credentials.


