Limits
Every account includes 5 concurrent browsers and 48 free browser hours. To raise either, email [email protected].
| Limit | Value | Counted as |
|---|---|---|
| Concurrent browsers | 5 | Sessions in state ACTIVE right now |
| Browser hours | 48 (172,800 seconds) | Lifetime total across every session you have ever run |
| Creates per minute | 60 | Create requests to /connect and POST /api/browser/create in a 60 second sliding window. Reconnects do not count |
| Session lifetime | 120 to 86,400 seconds | maxLifetime, default 7,200 |
| Minimum billed per session | 30 seconds | Every session, however short |
Browser hours are a lifetime total
Section titled “Browser hours are a lifetime total”The 48 hours are not a monthly allowance and they do not reset. Every session adds its full duration to the total when it ends, whatever state it ended in: closed by you, expired at its lifetime, or dead because the browser went away.
A session that dies at minute 40 of a planned 60 bills 40 minutes. Every session counts for at least 30 seconds, so 10 launches that each fail after 5 seconds use 300 seconds, not 50.
Running sessions are never killed at the cap. When the total is reached, the browsers you have keep going and new launches are refused.
Session lifetime
Section titled “Session lifetime”maxLifetime is seconds and it is clamped, not rejected. Ask for 30 and you get
120. Ask for a week and you get 86,400. 0 is treated as omitted. Nothing in the response tells you the
value moved, so read the expiresAt field on
the session rather than
assuming what you sent took effect.
Omitting it gives you 7,200 seconds, and all 7,200 of them count against your 48 hours if nothing closes the browser. A session outlives the CDP connection that created it, so a script that exits without closing keeps spending. Close sessions you are finished with.
Concurrency
Section titled “Concurrency”The sixth simultaneous browser is refused:
{ "error": "Concurrent browser limit reached", "code": "CONCURRENCY_LIMIT", "limit": 5, "active": 5}429, with retry-after: 5. A slot opens the moment any running session
reaches CLOSED, DEAD or EXPIRED.
Check before you launch with GET /user/api/limits. It does not count against
the rate limit:
curl https://browser.rayobyte.com/user/api/limits \ -H "x-api-key: rb_live_YOUR_KEY"{ "limit": 5, "active": 0, "remaining": 5, "unlimited": false }remaining also subtracts creates still in progress, so it can be lower than
limit minus active.
Rate limiting
Section titled “Rate limiting”More than 60 creates in 60 seconds is also 429, and it is a different thing.
Every create request counts, including ones refused with a 400 and ones the
rate limit itself refused, so a retry loop that hammers a 429 keeps the window
full. Reconnects do not count.
{ "error": "Rate limit exceeded", "code": "RATE_LIMIT", "limit": 60, "window": "60s"}Both rejections share the status code. Branch on code: CONCURRENCY_LIMIT or
RATE_LIMIT. Both bodies carry a limit, so that field alone does not tell
them apart.
The retry-after header is right for both, 5 seconds for concurrency and 60
for the rate limit.
Out of browser hours
Section titled “Out of browser hours”{ "code": "USAGE_CAP_REACHED", "remainingSeconds": 0}402. Retrying does not help. Contact
[email protected] and the cap is raised on your
account with no key change and no restart.
No proxy
Section titled “No proxy”400. A proxy is mandatory on every launch. The two create paths answer in
different shapes.
GET /connect answers in plain text, with the code in the X-Error-Code
response header and the rule that failed in parentheses:
HTTP/2 400x-error-code: PROXY_REQUIREDcontent-type: text/plain
A proxy is required for this account. Use your Rayobyte residential proxy or supply your own. (no proxy supplied)POST /api/browser/create answers in JSON:
{ "code": "PROXY_REQUIRED", "error": "A proxy is required for this account. Use your Rayobyte residential proxy or supply your own.", "detail": "no proxy supplied"}An empty string and a string of spaces both land here rather than in
PROXY_INVALID. A malformed proxy gives 400 PROXY_INVALID with the broken
rule as the detail, such as missing port.
Everything the API can refuse with
Section titled “Everything the API can refuse with”| Status | Code | Meaning | Worth retrying |
|---|---|---|---|
400 |
PROXY_REQUIRED |
No proxy on the request | No |
400 |
PROXY_INVALID |
Proxy present, malformed | No |
401 |
— | Missing or invalid API key | No |
402 |
USAGE_CAP_REACHED |
48 browser hours used | No |
422 |
PROXY_UNREACHABLE |
The browser could not connect through the proxy | No. Fix the proxy |
429 |
CONCURRENCY_LIMIT |
5 browsers already running | After retry-after, 5 seconds |
429 |
RATE_LIMIT |
More than 60 creates this minute | After retry-after, 60 seconds |
503 |
CAPACITY |
Every backend was full or unhealthy | Yes, retry-after is 2 seconds |
503 |
— | Authentication service temporarily unavailable |
Yes |
500, 502 |
— | Browser creation failed. The browser did not start |
Once |
The proxy is checked for form, not dialed, before a browser is created. A
well-formed proxy with the wrong password, or one that is down, fails when the
browser starts, and both create paths answer 422 with the code
PROXY_UNREACHABLE and this message:
The browser could not connect through that proxy. Check the proxy host, port, username and password.Retrying with the same proxy returns the same thing.
503 CAPACITY is the only one of these that is ours rather than yours. It means
no machine had a free seat, and the message is
All browsers are busy right now, try again in a moment.
On GET /connect, the 400, 422 and 503 codes arrive in the
X-Error-Code header with a plain-text body. 401, 402 and 429 are JSON on
both paths, because they are answered before the route runs.
SDK errors
Section titled “SDK errors”The current SDK releases (Python 2.2.0, Node.js 0.2.0) do not pass the code
through, and their exception class does not reliably say which of the two
limits was hit. Treat any 429 the same way: wait for the retry value, which
comes from the retry-after header and is right for both.
import timefrom rayobrowse import ConcurrencyLimitError, RateLimitError
try: ws_url = client.connect_url(os="windows", proxy=PROXY)except (ConcurrencyLimitError, RateLimitError) as e: time.sleep(e.retry_after) # 5 for a full account, 60 for the rate limitimport { ConcurrencyLimitError, RateLimitError } from 'rayobrowse';
try { const wsUrl = await client.connectUrl({ os: 'windows', proxy: PROXY });} catch (err) { if (err instanceof ConcurrencyLimitError || err instanceof RateLimitError) { await new Promise((r) => setTimeout(r, err.retryAfter * 1000)); } else { throw err; }}To know which limit it was, call the HTTP API directly and read code, or check
GET /user/api/limits.
Raising a limit
Section titled “Raising a limit”Email [email protected]. Concurrency and the hours cap are raised per account and take effect immediately.
Was this page helpful?
Thanks — that helps us fix it.