docs · errors & limits
Errors and limits
Every error from the gateway and the REST API has the same JSON shape, a stable machine-readable code and a message written for the person debugging — when a parameter is the problem, the message starts with its name.
The envelope
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json
{"error":{"code":"concurrency_limit_reached","message":"plan allows 2 concurrent sessions (2 active)","retryAfter":2}}Every 429 and 503 carries a Retry-After header in seconds, repeated as error.retryAfter in the body: Playwright's connectOverCDP error shows the body of a refused connection but not its headers. Branch on code, not on the message text.
Codes
| status | code | what to do |
|---|---|---|
| 400 | invalid_request | A value is malformed (bad proxy URL, invalid selector, body not JSON). The message names the field. |
| 400 | unsupported_param | An unknown parameter, or a Browserless feature we do not offer (profiles, stealth, recording, headful). See the compatibility table. |
| 400 | residential_proxy_not_available | No built-in proxy pool: pass your own with proxy=. See proxies. |
| 400 | playwright_protocol_not_supported | Use chromium.connectOverCDP(), not chromium.connect(). |
| 401 | unauthorized | The key is missing, wrong or revoked. |
| 403 | no_active_subscription | The account has no active plan, or a failed payment is past its grace period. |
| 403 | org_suspended | The account is suspended; the email with the statement of reasons explains why and how to contest it. |
| 403 | forbidden | The target is a private, loopback or metadata address. |
| 403 | target_blocked | The target hostname is on our abuse blocklist. |
| 404 | not_found | Unknown path. WebSocket 404s list the supported paths. |
| 404 | session_not_found | No active session with that ID in your account (it may have ended). |
| 408 | action_timeout | A REST action ran out of time. Raise timeout (max 180000). |
| 413 | invalid_request | The request body is too large. |
| 424 | navigation_failed | The target site failed (DNS, TLS, refused). The browser’s error is in the message. |
| 429 | concurrency_limit_reached | All your sessions are in use. Retry after Retry-After, pass queueTimeout, or upgrade. |
| 429 | rate_limited | Too many requests in a short window. Retry after Retry-After. |
| 503 | no_worker_available | Your workers are starting or being replaced. Retry after Retry-After. |
| 503 | worker_unreachable | A worker did not answer. Retry; tell us if it persists. |
| 503 | gateway_draining | The gateway is restarting for a deploy. Retry after Retry-After. |
| 5xx | internal | Our fault. Retry with backoff; tell us if it persists. |
WebSocket specifics
Before the upgrade, errors are ordinary HTTP responses with the envelope above — Puppeteer and Playwright surface them as Unexpected server response: 429. After the upgrade, a failure closes the socket with a code and a short reason:
| close code | meaning |
|---|---|
| 1000 | Normal close: the session ended (your close, DELETE, idle timeout or maximum duration). |
| 1001 | The gateway is restarting. A keepalive or REST-created session keeps running: the reason names the /session/<id> path to re-attach to. |
| 1008 | Policy: the API key was revoked, the account was suspended or closed, or the plan is no longer active. |
| 1009 | A message from your client was larger than 64 MB. |
| 1011 | Unexpected error on our side. |
| 1013 | Try again later: the browser could not be reached. |
Retrying
// connect with a bounded retry that honours Retry-After
async function connectWithRetry(url, attempts = 5) {
for (let i = 0; ; i++) {
try {
return await puppeteer.connect({ browserWSEndpoint: url });
} catch (err) {
// puppeteer/ws report the handshake status in the message, e.g.
// "Unexpected server response: 429"
const status = Number(/response: (\d{3})/.exec(String(err))?.[1]);
if (i >= attempts || ![429, 503].includes(status)) throw err;
await new Promise((r) => setTimeout(r, 2 ** i * 1000));
}
}
}
// or let the gateway wait for you: append &queueTimeout=15000 to the URLLimits
| limit | value |
|---|---|
| concurrent sessions | Solo 2 · Team 6 · Scale 20 |
| session duration | 12 h maximum (default and cap) |
| idle timeout | 5 min default for REST-created sessions |
| REST action | 60 s default, 180 s maximum (timeout=) |
| queueTimeout | 60 s maximum |
| request body | 256 KB on /v1, 5 MB on the Browserless aliases |
| scrape | 32 selectors, 2,000 matches each, 30 s wait per selector |
| screenshot viewport | 7680 × 4320 px |
| launch JSON | 4,096 characters, 32 Chromium flags |
| proxy URL | 2,048 characters |
| CDP message | 64 MB client → browser, 256 MB browser → client |
Fair use
Hours and pages are not metered. The Terms add a fair-use rule on sustained traffic volume far outside normal automation patterns, applied only after notice to you — and never to exporting your data or switching provider.