Rate limits
Driftstack uses a per-account token-bucket scheme to keep one noisy customer from degrading the platform for everyone else. This page documents the buckets, the response headers your client should consume, and how to react to 429s gracefully.
How the buckets work
Each request increments a counter against one or more named
buckets. Buckets refill at a tier-dependent rate. If any bucket
is empty when your request lands, you get a 429 + a
Retry-After header.
Buckets are per account, not per API key. If you mint 10 keys to spread your load, you're still hitting the same buckets — the limit is on the account.
Bucket reference
Seven buckets are enforced today: global (every
authenticated request), sessions:create (POST
/v1/sessions), agent_sessions:message (POST
/v1/agent-sessions/:id/message), agent_sessions:steps
(POST /v1/agent-sessions/:id/steps, the lists of steps you write),
agent_sessions:tabs (POST /v1/agent-sessions/:id/tabs,
the tab calls, with the same figures as the steps),
agent_sessions:input_event (the desktop app's input
stream while a person drives a session by hand), and
agent_sessions:live (what a live window reads and
controls on one agent session). Burst is the maximum bucket
capacity; sustained throughput is the refill rate. All six are
returned by GET /v1/account/rate-limits and named in
the X-RateLimit-Bucket header. A request your dashboard makes
while you are signed in can also carry dashboard: the
separate limit for browsing, which never uses your plan's API
allowance. The header can also name global_ip,
ingress or webhooks:send (see Response
headers below), so do not reject a value you do not know.
| Bucket | Starter | Scale | What it covers |
|---|---|---|---|
global | 120 burst · 120 req/min | 1,800 burst · 1,800 req/min | Every authenticated request increments this bucket. The default rate-limit catch-all. |
sessions:create | 10 burst · 2 req/min | 60 burst · 60 req/min | POST /v1/sessions — burst-sensitive; throttled tighter than `global` because starting a browser is expensive. |
agent_sessions:message | 40 burst · 20 req/min | 300 burst · 180 req/min | POST /v1/agent-sessions/:id/message — isolated from global so an LLM-driven message loop can't drain the global cap. |
agent_sessions:input_event | 360 burst · 5,400 req/min | 600 burst · 9,000 req/min | Keystrokes and pointer events the Driftstack desktop app sends while a person drives a session by hand. Sized for a human input stream, so it is far wider than the other buckets; it is separate so a 120Hz input stream cannot burn your generic-API quota. |
agent_sessions:live | 240 burst · 240 req/min | 2,880 burst · 2,880 req/min | What a live window reads and controls on one agent session: GET /v1/agent-sessions/:id and its page state, network log, cookies, downloads and frames, plus mode, takeover, handback, stop, the live video token and the transport report. Separate from global so list calls can't slow a session you are watching; it grows with the sessions your plan runs at once. |
agent_sessions:steps | 40 burst · 30 req/min | 480 burst · 360 req/min | POST /v1/agent-sessions/:id/steps — the lists of steps you write. They run no AI, so they have their own bucket, apart from messages; it grows with the sessions your plan runs at once. |
agent_sessions:tabs | 40 burst · 30 req/min | 480 burst · 360 req/min | POST /v1/agent-sessions/:id/tabs — the tab calls (list, open, switch, close). Their own limit with the same figures as agent_sessions:steps, so tab calls never use up your steps. |
The plans between them sit between these sizes, the free trial has smaller buckets, and Enterprise buckets are set by contract. Every plan's buckets are in the rate-limit reference.
Response headers
An API response carries these headers for one rate-limit bucket
the request drew on (on a call made for a team owner, yours, never
the owner's): its route's bucket or, for a request
refused before that (a 401, or a 403 for a missing scope), the
per-address global_ip bucket (or, for a signed-in
call, your account's ingress bucket where per-account
counting took it). A webhook test or replay refused by its own
webhooks:send limit carries that bucket's headers. A
429 from a team owner's own limit, on a call made for that owner,
carries none of them, only Retry-After. The limit,
remaining and reset values are also sent as
ratelimit-limit,
ratelimit-remaining and ratelimit-reset
(that reset is in seconds from now):
| Header | Value |
|---|---|
X-RateLimit-Limit | The bucket capacity (tokens). |
X-RateLimit-Remaining | Tokens left after this request. |
X-RateLimit-Reset | Unix seconds when the bucket will be full again. |
X-RateLimit-Bucket | The bucket name (e.g. sessions:create). |
Track X-RateLimit-Remaining against a low-water
mark in your client; if it drops below, say, 20% of the limit,
slow your request rate proactively rather than waiting for the
429.
Sign-up has a daily limit per IP address, because it happens
before there is an API key: at most 25 sign-up requests from one
IP address in any rolling 24 hours, counting requests later
refused for bad input or an email already registered. A sign-up
request that is let through also carries
X-RateLimit-Daily-Remaining (requests left for that
address in the 24 hours) and X-RateLimit-Daily-Reset
(Unix seconds 24 hours after the newest request, when all 25 are
free again). A slot frees sooner, 24 hours after each request:
once the limit is reached, the 429's Retry-After says
when the oldest one leaves the window. Only sign-up sends these
two headers.
When you hit a 429
The body follows RFC 9457 (application/problem+json) —
flat keys, no error envelope. The type
URI dereferences to the long-form explanation; the
retry_after_seconds extension carries the same value
as the Retry-After header for clients that prefer to
read it from the body.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Bucket: sessions:create
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715472012
Content-Type: application/problem+json
{
"type": "https://errors.driftstack.dev/rate-limited",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit for \"sessions:create\" exceeded for tier \"starter_v3\".",
"retry_after_seconds": 12
} Your client should:
- Stop sending requests to the same bucket immediately.
- Wait at least
Retry-Afterseconds. - Resume with reduced concurrency; gradually ramp back up.
Do not retry-loop without backoff. We log sustained 429s as abuse and may rate-limit further or temporarily disable your key.
Concurrent sessions
Separate from per-bucket request rate, your tier caps the number of concurrently running sessions (how many run at once). See /docs/concurrency for the authoritative table + backoff guidance.
Exceeding the concurrency cap returns
429 Too Many Requests with the
https://errors.driftstack.dev/concurrency-limit
problem-type — distinct from rate-limit 429s, which use the
https://errors.driftstack.dev/rate-limited type.
Dispatch on the type URI, not the status code.
Requesting a temporary override
For one-off events (load tests, customer-facing demos), email [email protected] describing the bucket(s) + multiplier + duration you need. We generally approve up to 5× for up to 48h on the spot for paid tiers. Anything larger needs a quick conversation about why.
Overrides are visible in your dashboard on the Usage page, under Limits for your account. They expire automatically at the configured time.
Testing rate-limit behaviour locally
If you run Driftstack self-hosted, it returns the same 429 responses as the cloud service, so you can exercise your 429 handling there without using your production budget. See Self-hosted for details.
Support
Sustained 429s that you didn't expect, or a need to discuss a production-impacting limit: [email protected].