Browserless API Rate Limits for Bulk Webpage Screenshots
Browserless screenshot capacity is governed by concurrent browser sessions, not one universal requests-per-minute quota. Learn how to cap bulk jobs, handle 429s, and track usage.
Direct answer: Browserless does not publish one universal Cloud requests-per-minute limit for bulk screenshots. The practical limit is the maximum number of browser sessions that can run at once, and on Browserless Cloud that value depends on your plan. Check the concurrency limit in your account dashboard, cap parallel screenshot requests at or below it, and adjust after observing queueing, rejections, and peak concurrency. A burst of simultaneous calls is not a guaranteed sustained request rate. Browserless describes concurrency and caller-side limits in its concurrent-session guidance.
This guide shows the screenshot request format, bounded bulk scheduling, retry handling, monitoring, and how the rules differ for Cloud and self-hosted deployments.
1. Understand which limit you are hitting
Each call to Browserless’s screenshot REST endpoint starts a browser session, navigates to a page, captures an image, and ends the session. The number that determines how many of those jobs can run at the same time is concurrency. Browserless Cloud concurrency is plan-specific; find your account’s effective limit in the Cloud Subscription card on the dashboard. Do not infer it from another account, a generic example, or a requests-per-minute guess. The dashboard documentation describes the concurrency and usage cards.
| Term | What it means | What to do |
|---|---|---|
| Concurrency | Browser sessions running simultaneously | Read your plan’s limit and set your client cap accordingly. |
| Queue | Requests waiting for a free session slot | Allow queueing to absorb modest bursts; do not assume the queue is unlimited. |
| HTTP 429 | A request was rejected in a concurrency/queue context | Reduce offered load, allow a delay, and retry with a cap. |
| Monthly units | Cloud usage allowance, separate from moment-to-moment concurrency | Track it separately from parallelism. |
| Session duration | How long a browser stays open for a job | Keep navigation and waiting work bounded; close sessions promptly. |
These constraints are related but not interchangeable. Browserless Cloud meters browser time in 30-second increments: its example says a 31-second session consumes two units, while a 30-second session consumes one. A request rejected before a browser starts, such as a full-queue 429, consumes no browser-time units; a failure after a session begins can consume the time already used. See Browserless’s unit-consumption documentation for the current metering explanation.
2. Make one screenshot request
The current REST API uses POST /screenshot, a token query parameter, and a JSON body. The response is image bytes. fullPage captures the whole document; omit it or set it to false for the viewport. PNG, JPEG, and WebP are supported by the current API. Examples below use the documented production endpoint; use the region endpoint and token supplied for your account where applicable. See the Screenshot API reference for the full request options.
cURL
curl --fail-with-body -X POST \
'https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Cache-Control: no-cache' \
-d '{"url":"https://example.com/","options":{"fullPage":true,"type":"png"}}' \
--output screenshot.png
Python
import requests
TOKEN = "YOUR_API_TOKEN"
endpoint = "https://production-sfo.browserless.io/screenshot"
payload = {
"url": "https://example.com/",
"options": {"fullPage": True, "type": "png"},
}
response = requests.post(
endpoint,
params={"token": TOKEN},
headers={"Cache-Control": "no-cache"},
json=payload,
timeout=90,
)
response.raise_for_status()
with open("screenshot.png", "wb") as image:
image.write(response.content)
Node.js
import { writeFile } from "node:fs/promises";
const endpoint = new URL("https://production-sfo.browserless.io/screenshot");
endpoint.searchParams.set("token", process.env.BROWSERLESS_TOKEN ?? "YOUR_API_TOKEN");
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" },
body: JSON.stringify({
url: "https://example.com/",
options: { fullPage: true, type: "png" },
}),
signal: AbortSignal.timeout(90_000),
});
if (!response.ok) {
throw new Error(`Browserless returned HTTP ${response.status}: ${await response.text()}`);
}
await writeFile("screenshot.png", Buffer.from(await response.arrayBuffer()));
Keep the token out of source control and logs. In production, load it from a secret store or environment variable. Check the HTTP status before saving a response: an error body is not a valid image.
3. Schedule a bulk job with a concurrency cap
Use a bounded worker pool rather than launching every URL at once. Start at or below the dashboard’s concurrency limit. If other tasks share the account, reserve capacity for them. A lower cap can also protect target websites: Browserless capacity does not mean every destination will accept the same rate.
Python worker pool
import os
import time
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
TOKEN = os.environ["BROWSERLESS_TOKEN"]
ENDPOINT = "https://production-sfo.browserless.io/screenshot"
URLS = ["https://example.com/", "https://www.iana.org/", "https://www.python.org/"]
CONCURRENCY = 3 # Set at or below the limit shown in your account dashboard.
def capture(index, page_url):
payload = {"url": page_url, "options": {"fullPage": True, "type": "png"}}
for attempt in range(4):
try:
response = requests.post(
ENDPOINT,
params={"token": TOKEN},
json=payload,
timeout=90,
)
if response.status_code == 429 and attempt < 3:
time.sleep(min(8, 0.5 * (2 ** attempt)))
continue
response.raise_for_status()
filename = f"shot-{index}.png"
with open(filename, "wb") as image:
image.write(response.content)
return page_url, filename
except requests.RequestException:
if attempt == 3:
raise
time.sleep(min(8, 0.5 * (2 ** attempt)))
with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
futures = [pool.submit(capture, i, url) for i, url in enumerate(URLS, 1)]
for future in as_completed(futures):
print(future.result())
Node.js worker pool
import { writeFile } from "node:fs/promises";
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error("Set BROWSERLESS_TOKEN");
const endpoint = "https://production-sfo.browserless.io/screenshot";
const urls = ["https://example.com/", "https://www.iana.org/", "https://www.python.org/"];
const concurrency = 3; // Set at or below your dashboard limit.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function capture(index, pageUrl) {
for (let attempt = 0; attempt < 4; attempt++) {
try {
const api = new URL(endpoint);
api.searchParams.set("token", token);
const response = await fetch(api, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: pageUrl, options: { fullPage: true, type: "png" } }),
signal: AbortSignal.timeout(90_000),
});
if (response.status === 429 && attempt < 3) {
await sleep(Math.min(8000, 500 * (2 ** attempt)));
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const filename = `shot-${index}.png`;
await writeFile(filename, Buffer.from(await response.arrayBuffer()));
return { url: pageUrl, filename };
} catch (error) {
if (attempt === 3) throw error;
await sleep(Math.min(8000, 500 * (2 ** attempt)));
}
}
}
let next = 0;
async function worker() {
while (next < urls.length) {
const index = next++;
console.log(await capture(index + 1, urls[index]));
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, worker));
The examples retry transient failures a limited number of times and use exponential delays for 429 responses. For a production queue, add jitter to retry delays, persist failed URL/task IDs, and make output names stable or unique. Avoid retrying malformed requests (usually 400/422) unchanged; they will continue to fail. Avoid indefinite retries on 5xx or network errors.
Choosing the starting cap
- Read the current Cloud concurrency limit in the account dashboard.
- Set worker concurrency to that value or lower. Leave headroom if other users or services share the account.
- Run a representative batch, including the slowest page types you expect.
- Review peak concurrency and rejected requests. If rejections appear, lower offered concurrency or smooth bursts. If there is stable headroom and the target sites tolerate the traffic, cautiously raise the cap within the account limit.
- Repeat during realistic workload periods; a short burst does not establish sustained throughput.
4. Tune screenshot work without hiding capacity problems
Concurrency controls the number of simultaneous sessions. The time each session remains open affects how quickly slots free up and, on Cloud, how many browser-time units the work consumes. A viewport screenshot of a fast page may finish sooner than a full-page capture that waits for client-side rendering or images. Browserless documents screenshot options and wait/navigation controls, but does not promise fixed per-image latency.
- Keep the capture scope intentional. Use viewport capture when a full-page image is unnecessary. Full-page captures can require more rendering and produce larger files.
- Wait only for what the page needs. Waiting for selectors, events, or network activity can improve completeness but can lengthen sessions. Prefer a meaningful selector or bounded wait over an unbounded wait.
- Choose the output deliberately. PNG preserves lossless detail; JPEG quality can trade file size for image fidelity; WebP is available on the current screenshot endpoint. Format changes output size, not the account concurrency limit.
- Limit destination load. Group requests by destination if needed and set a lower per-host cap for sites with strict limits. Honor the target site’s terms and access controls.
- Do not increase concurrency to fix slow pages blindly. Longer sessions occupy slots longer. Diagnose navigation, asset loading, or selector waits first.
Relevant screenshot configuration includes options.fullPage, options.type, options.quality (for lossy formats), options.clip, viewport-related settings, and selector capture. The endpoint also supports shared wait behavior, navigation settings such as gotoOptions, resource filtering through rejectResourceTypes or rejectRequestPattern, and bestAttempt for continuing after selected wait errors. Consult the API reference before relying on an option, since request-level screenshot settings and launch parameters serve different purposes.
5. Monitor usage, queueing, and failures
Use the dashboard to inspect rejected requests, peak concurrency, timed out requests, errors, time units, monthly usage, and the account’s concurrency limit. The dashboard documentation says activity charts refresh every five minutes, with a manual refresh option. A rejected-request spike often means the caller should queue work or the plan needs more concurrency. Also check whether a free plan has run out of units, which can cause rejections for a different reason.
For automated usage monitoring, Browserless documents an account usage endpoint. Treat usage/allowance alerts separately from your worker cap; monthly units and concurrent sessions answer different questions. See the usage API and metering guide.
6. Cloud versus self-hosted limits
Do not apply self-hosted defaults to Browserless Cloud. For Cloud, consult the account dashboard and plan. For self-hosted deployments, the effective capacity depends on the product generation, deployment settings, and machine resources.
| Deployment | What the documentation says | How to use the information |
|---|---|---|
| Browserless Cloud | Plan determines simultaneous sessions; dashboard shows the account’s concurrency limit. | Use the account-specific value. |
| Current self-hosted Docker configuration | Current configuration documents CONCURRENT and QUEUED; current docs show defaults of 10 for each. |
Read the configuration for your running version and provisioned hardware; do not call this a Cloud limit. |
| Legacy BaaS v1 Docker | Legacy page documents a default maximum of 5 sessions and queue length of 5. | Use only when identifying that legacy deployment context. |
In current Docker deployments, running sessions plus queued requests determine how many connections can be accepted before a full queue returns 429. The current Docker configuration reference advises starting conservatively and scaling based on hardware capacity. The legacy default is documented on the BaaS v1 Docker page; the older and current defaults are not interchangeable.
7. Troubleshooting common bulk screenshot failures
| Symptom | Likely cause | Fix |
|---|---|---|
| HTTP 429 | Concurrency or queue capacity was reached; account usage limits can also reject work. | Reduce client concurrency, smooth bursts, retry with bounded exponential backoff, then check rejected-request metrics and the Cloud Subscription card. |
| Requests wait longer than expected | Sessions are queued behind active jobs, or the page/capture takes longer to finish. | Use a bounded queue, measure complete session duration, and shorten unnecessary waits or capture scope. |
| HTTP 401/403 | Missing, invalid, expired, or incorrectly passed token; possibly wrong endpoint/account configuration. | Verify the token and regional endpoint in account settings. Keep credentials in the query parameter as required by this API. |
| HTTP 400/422 | Invalid JSON or unsupported/misplaced request option. | Validate the JSON, URL, and option names against the current Screenshot API schema. Do not retry unchanged input. |
| Timeout or 5xx | Slow destination, navigation failure, heavy rendering, resource pressure, or transient service/network issue. | Set a finite client timeout, inspect page-specific failures, retry transient errors with a small limit, and reduce concurrency if errors cluster under load. |
| Saved file is HTML or appears corrupt | Error response was written as if it were image bytes. | Check HTTP status and content type before writing; log a bounded error body for diagnosis. |
| Blank image, CAPTCHA, or access denied page | The target site served a challenge or blocked automated traffic. | Confirm behavior is allowed by the target site and inspect the returned capture. Increasing concurrency does not resolve bot checks. |
| Missing content in full-page shot | Lazy content may not have loaded, or capture occurred before the relevant selector/event. | Use documented scrolling/wait controls where appropriate and verify the chosen wait condition. |
| Dashboard count seems inconsistent | Charts may be scoped to a time window or API token, or may not yet have refreshed. | Check the selected token/window, refresh, and compare with the billing-cycle subscription card. |
Browserless’s retry guidance distinguishes retryable capacity/network problems from invalid input. A full-queue 429 rejected before browser launch consumes no browser-time units; failures after a browser starts may consume the time already used.
8. Or skip the browser setup
If you need screenshots rather than a managed browser session, ScreenshotNeo provides a one-request screenshot API and an MCP server for AI agents. It removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed; and its MCP tools let agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Every feature is on every plan. 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,
)
r.raise_for_status()
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(`HTTP ${res.status}: ${await res.text()}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', image));
Start with 1,000 free screenshots each month, with no card required.
9. Frequently asked questions
How many Browserless screenshots can I request at once?
Use the concurrency limit shown for your account as the ceiling for simultaneous browser sessions. There is no universal Cloud number that applies to every plan.
Is the limit requests per minute?
The official Cloud guidance reviewed for this article specifies plan-dependent concurrency, queues, and monthly units; it does not establish one account-independent screenshots-per-minute quota.
Does a 429 use Browserless units?
A request rejected before a browser starts, such as one rejected by a full queue, consumes no browser-time units. A request that begins and then fails can consume the session time already used.
Can I raise the Cloud concurrency limit myself?
Check the plan and account options shown in your dashboard. The available limit is plan-specific; do not assume a self-hosted configuration value applies to Cloud.
Will a lower concurrency always make a bulk job cheaper?
It limits simultaneous sessions, but total browser time depends on how long each session runs. Track actual usage as well as parallelism.


