Rate limits
Request rate buckets, concurrency quotas, headers, and how to behave when limited.
Two different ceilings, often confused:
| Rate limit | Concurrency quota | |
|---|---|---|
| Limits | Requests per unit time | Simultaneously running sessions |
| Resets | Continuously, by token refill | When a session ends |
| Error | 429 rate_limited | 429 quota_exceeded |
| Retry | Yes, after retry_after_s | Yes, when something finishes |
| Protects | The API | The fleet, and your bill |
Rate limits
Token bucket per (organization, route class), with a smaller per-key burst bucket so one
misbehaving key cannot consume the organization's whole budget.
| Route class | Routes | Default | Burst |
|---|---|---|---|
read | GET on anything | 1000 / min | 100 |
session_write | Create, update, delete sessions | 600 / min | 60 |
exec | Runs and exec | 3000 / min | 300 |
fs | Filesystem | 2000 / min | 200 |
tool | Tool invocation | 6000 / min | 600 |
browser, desktop | Action batches | 3000 / min | 300 |
admin | Keys, policies, members | 60 / min | 10 |
usage | Usage and billing queries | 120 / min | 20 |
Streaming connections count once at establishment, not per frame. A PTY open for an hour is one
request against exec.
Headers
Every response carries them, including successful ones:
RateLimit-Limit: 600 RateLimit-Remaining: 573 RateLimit-Reset: 41 RateLimit-Policy: 600;w=60;burst=60;comment="session_write"
HTTP/1.1 429 Too Many Requests Retry-After: 12 RateLimit-Remaining: 0 RateLimit-Reset: 12
Watch RateLimit-Remaining and slow down before hitting zero. Clients that only react to a 429
oscillate between saturating and stalling.
Concurrency quotas
{
"concurrent_sessions": { "tier0": null, "tier1": 500, "tier2": 100, "tier3": 50, "tier4": 10 },
"session_creates_per_minute": 600,
"storage_gib": 10000,
"egress_gb_per_day": 2000
}
Tier 0 has no concurrency limit — there is no session to hold, only the tool rate class.
Quotas are per organization by default, and can be subdivided per project and per end user. A reseller that does not subdivide has one customer able to exhaust the quota for all of them.
curl -sS -X PATCH "$WB_BASE_URL/end-users/eu_customer_4821" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "quotas": { "concurrent_sessions": { "tier2": 5, "tier3": 2, "tier4": 1 } } }'
Plan ceilings
| Developer | Team | Scale | Enterprise | |
|---|---|---|---|---|
| Tier 1 concurrency | 20 | 200 | 1000 | Negotiated |
| Tier 2 concurrency | 5 | 50 | 300 | Negotiated |
| Tier 3 concurrency | 2 | 20 | 100 | Negotiated |
| Tier 4 concurrency | 0 | 5 | 25 | Negotiated |
| Creates / minute | 60 | 300 | 1200 | Negotiated |
| Storage | 100 GiB | 2 TiB | 20 TiB | Negotiated |
| Egress / day | 50 GiB | 500 GiB | 5 TiB | Negotiated |
Raise within the plan ceiling with PATCH /v1/quotas. Beyond it is a conversation, because the
ceiling reflects capacity that has actually been provisioned — particularly for tier 4, where
concurrency maps one-to-one onto physical machines.
Handling limits well
Respect Retry-After
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? 1) * 1000
await sleep(wait + Math.random() * 500) // jitter
return retry()
}
Ignoring Retry-After and retrying immediately gets the bucket throttled harder.
Bound client-side concurrency
import pLimit from 'p-limit'
const limit = pLimit(40) // below the tier 1 quota of 500, leaving headroom
await Promise.all(tasks.map((t) => limit(() => wb.runs.create({ tier: 1, code: t.code }))))
Firing 500 creates at a 500-session quota guarantees some fail. Run below the ceiling.
Queue rather than retry-storm
For bulk work, a queue with a steady drain rate beats a burst plus exponential backoff. It finishes
sooner and produces no 429s at all.
Treat quota_exceeded as backpressure
if (e.code === 'quota_exceeded') {
await waitForSessionSlot() // wait on a session.stopped webhook, not a timer
return retry()
}
Waiting on an event is better than polling: the slot frees the moment a session ends.
Separate keys for separate workloads
Per-key burst buckets mean a batch job and an interactive path on different keys do not starve each
other. An interactive agent sharing a key with a nightly batch will see 429s caused entirely by
the batch.
Gateway protections
Beyond the documented limits, the gateway applies protections that are not published as numbers because publishing them would be a roadmap for abuse:
- Per-IP connection limits
- Request body size ceilings
- Slowloris and slow-body protections
- Anomaly detection on sudden pattern changes per key
- Automatic throttling of keys with pathological error rates
Normal usage never encounters these. Hitting one returns 429 with code: "rate_limited" and no
retry_after_s; back off substantially and, if it recurs, open a support request with the
request_id.
Monitoring your own usage
curl -sS "$WB_BASE_URL/usage/timeseries?period=today&bucket=1m&metric=api_requests&group_by=route_class" \ -H "Authorization: Bearer $WB_API_KEY"
The customer console graphs the same data with the limit drawn on the chart, which is the easiest way to see that an agent loop is running two orders of magnitude hotter than intended. See customer console.