Why Does Browserless Screenshot API Return HTTP 429?
A Browserless screenshot API 429 means the request queue is full or the service is over capacity. Learn how to limit concurrency, retry safely, and diagnose deployment-specific limits.
Short answer: Browserless returns HTTP 429 when its request queue is full or the service is over capacity. Limit simultaneous screenshot requests, let pending work drain, and retry rejected requests with bounded exponential backoff. On Enterprise or self-hosted deployments, check the configured concurrency and queue limits; the settings differ by deployment and product generation.
1. What HTTP 429 means for Browserless screenshots
Browserless describes 429 as a capacity signal: requests can wait while queue space remains, but requests beyond the configured running-plus-pending capacity are rejected. Its API reference describes 429 as too many requests currently being processed. A burst of screenshot jobs can therefore fail even if each individual capture is valid.
First confirm the request is going to the current screenshot REST endpoint: POST /screenshot, with the token in the query string and screenshot options in a JSON body. Check the HTTP status before interpreting the response as image bytes. See the Browserless screenshot API documentation.
2. Diagnose the response before changing settings
- Record the status code and response headers for a failed call. Do not try to open a 429 response as PNG or JPEG data.
- Confirm the endpoint and authentication format match the current screenshot API documentation.
- Check whether failures coincide with bursts or high parallelism. A queue-capacity 429 is a signal to slow admission and allow existing work to finish.
- Identify the deployment type: managed service, Enterprise or self-hosted deployment, or legacy BaaS v1. The applicable controls and setting names differ.
- For managed accounts, check the applicable account dashboard and service status. Public documentation cannot reveal your account’s live allowance or queue state.
Do not apply the queue remedy to every error. Browserless documents 401 for missing or invalid authorization, 403 for a disallowed destination, 408 for timeout, 500 for an internal error, and 503 for unavailable service. Diagnose the status you actually received using the relevant endpoint documentation.
3. Limit concurrency and retry with backoff
Use a fixed concurrency limit for screenshot workers rather than launching one request per URL at once. When a request receives 429, wait and retry a limited number of times with exponential backoff and jitter. Stop after the retry budget and surface the failure for later handling; an unbounded retry loop can keep the queue saturated.
The following is runnable Node.js using the built-in fetch. It posts a URL and optional screenshot settings, checks status before saving image bytes, and retries only 429 responses. Set BROWSERLESS_TOKEN in the environment. The semaphore-like worker pool bounds simultaneous requests.
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set BROWSERLESS_TOKEN');
const endpoint = new URL('https://production-sfo.browserless.io/screenshot');
endpoint.searchParams.set('token', token);
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function capture(url) {
const body = {
url,
options: { fullPage: true, type: 'png' }
};
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
if (response.ok) {
const bytes = Buffer.from(await response.arrayBuffer());
const name = new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_');
const fs = await import('node:fs/promises');
await fs.writeFile(`${name}.png`, bytes);
return;
}
const detail = await response.text();
if (response.status !== 429 || attempt === 4) {
throw new Error(`Screenshot failed: HTTP ${response.status}: ${detail}`);
}
const retryAfter = Number(response.headers.get('retry-after'));
const exponential = Math.min(30_000, 500 * (2 ** attempt));
const jitter = Math.floor(Math.random() * 250);
await sleep((Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : exponential) + jitter);
}
}
async function runPool(items, limit, task) {
let next = 0;
async function worker() {
while (true) {
const index = next++;
if (index >= items.length) return;
await task(items[index]);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
}
await runPool(['https://example.com', 'https://example.org'], 3, capture);
Use the endpoint host assigned to your Browserless region or account as documented for your deployment. The example’s host is illustrative; do not assume it is the host assigned to every account. Keep tokens out of source control and logs.
4. Deployment-specific queue settings
| Deployment | What to inspect | Important boundary |
|---|---|---|
| Managed Browserless | Account dashboard, applicable plan allowance, and service status. | Public documentation does not establish an individual account’s live capacity. |
| Enterprise or self-hosted | CONCURRENT controls maximum concurrent sessions; QUEUED controls maximum queued requests. Enterprise documentation lists defaults of 10 concurrent sessions and 10 queued requests. |
Requests exceeding the available running and pending capacity are rejected. Raise limits only in line with available resources. |
| Legacy BaaS v1 Docker | Legacy documentation uses MAX_QUEUE_LENGTH and lists a default queue length of five. |
BaaS v1 is marked no longer actively supported. Do not apply its variable name or default to current deployments. |
For Managed Private Deployment, Browserless says settings are adjusted in the account dashboard. Check the documentation for the exact deployment before changing values: Enterprise connection settings and legacy BaaS Docker documentation.
5. Common causes and fixes
| Symptom | Likely cause | What to do |
|---|---|---|
| 429 appears during a batch run | The client starts more captures at once than the service can run or queue. | Cap worker concurrency, split the batch, and use bounded backoff. |
| 429 repeats immediately after each retry | Retries are too fast, unlimited, or all workers retry together. | Add exponential delay and jitter, limit attempts, and let active jobs drain. |
| 429 continues at low client concurrency | The account or deployment may have less capacity available, or other clients may be consuming it. | Check dashboard/account details or self-hosted telemetry; review total traffic across clients. |
Changing MAX_QUEUE_LENGTH has no effect |
That variable belongs to legacy BaaS v1, not the current Enterprise configuration. | Verify the product generation and use its documented controls. |
| The body cannot be decoded as an image | The response is an error body, not screenshot bytes. | Check status first, then read error text for non-success responses. |
| The status is 401, 403, 408, 500, or 503 | This is a different failure class. | Check token, destination policy, timeout, service error, or availability respectively. |
6. Reliability, performance, and cost considerations
Reliability: Put a bounded queue in your own application so upstream bursts do not become Browserless bursts. Persist failed jobs if captures matter, distinguish retryable 429/temporary availability errors from configuration errors, and log status, attempt count, and request identifiers when available. Avoid logging the token or sensitive page data.
Performance: More client concurrency can improve throughput only until the service’s available capacity is reached. Beyond that point, it increases queue pressure and retries rather than completed screenshots. Tune concurrency gradually against your own workload and deployment telemetry; the public sources reviewed do not establish a universal safe rate or throughput benchmark.
Cost: The dossier does not establish Browserless pricing or whether a particular rejected request is billable. Check the terms and usage reporting for your account before estimating the cost of retries. Keep retry counts finite so a prolonged capacity issue does not create unnecessary traffic.
7. Or skip the browser setup
If you need screenshots without managing a browser session and queue, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. Its capture options include full-page and selector capture, custom waits, headers and cookies, device presets, and more; see the ScreenshotNeo API docs.
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)
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}`);
- Cookie banners are accepted and removed before capture; known consent banners, newsletter popups, and chat widgets are removed, and each step can be disabled.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdffor AI agents. - The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
8. FAQ
Does a 429 mean my screenshot request is malformed?
Usually it points to capacity or queue pressure. Still verify the endpoint and inspect the response status and body before assuming the request reached the intended API.
Can I fix a managed account’s capacity from my client?
You can reduce the load your client creates with concurrency limits and backoff. Account-specific capacity and live queue state must be checked through the applicable account controls.
Should I retry 403 or 401 the same way as 429?
No. A 401 or 403 indicates authorization or destination policy concerns in the documented API status list; fix those conditions instead of retrying unchanged.


