Skip to main content

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 StarterScale What it covers
global 120 burst · 120 req/min1,800 burst · 1,800 req/min Every authenticated request increments this bucket. The default rate-limit catch-all.
sessions:create 10 burst · 2 req/min60 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/min300 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/min600 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/min2,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/min480 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/min480 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):

HeaderValue
X-RateLimit-LimitThe bucket capacity (tokens).
X-RateLimit-RemainingTokens left after this request.
X-RateLimit-ResetUnix seconds when the bucket will be full again.
X-RateLimit-BucketThe 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:

  1. Stop sending requests to the same bucket immediately.
  2. Wait at least Retry-After seconds.
  3. 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].