Drıftstack docs
Docs

Rate limits

Driftstack enforces per-tier token-bucket rate limits on every authenticated /v1/* call. The limits are intentional anti-abuse caps (runaway scripts, accidental DoS), not the pricing meter. Pricing is based only on how many sessions run at once (concurrent sessions).

Seven bucket keys

Every authenticated request selects exactly one bucket key:

  • global — every authenticated /v1/* call that doesn’t have a dedicated bucket below.
  • sessions:create — the two session-creating calls, POST /v1/sessions and POST /v1/profiles/:id/launch. Starting or reopening an agent session draws on it too: POST /v1/agent-sessions and POST /v1/agent-sessions/:id/reopen. Lower cap because starting a browser is the most expensive operation; a profile launch creates a session too, so it draws on the same cap rather than a separate one. A create that is refused after the limit is checked (a missing proxy, no free device, the concurrency limit) still counts: the limit is on create calls, not on sessions started.
  • agent_sessions:message — POST /v1/agent-sessions/:id/message. Isolated from global so an LLM-driven message loop can’t drain the global cap and exhaust unrelated API capacity.
  • agent_sessions:steps — POST /v1/agent-sessions/:id/steps (a list of steps you write). Lists of steps run no AI, so they have their own bucket, apart from messages: steps cannot use up your messages, and messages cannot use up your steps or your tab calls. It grows with the number of sessions your plan runs at once.
  • agent_sessions:tabs — POST /v1/agent-sessions/:id/tabs (tab calls: list, open, switch, close). Tab calls have their own limit with the same figures as agent_sessions:steps, so tab calls and lists of steps cannot use up each other, and neither uses up your messages.
  • agent_sessions:input_event — the desktop app’s live input into a session a person is driving. Sized for high-frequency input; isolated so an input stream can’t drain the global cap. Delivery to the session has its own, lower limit.
  • agent_sessions:live — what a live window reads and controls on one agent session, so list calls and the rest of your traffic on global can’t slow a session you are watching. It grows with the number of sessions your plan runs at once; listing agent sessions (GET /v1/agent-sessions) stays on global.
    • Reads on agent_sessions:live: GET /v1/agent-sessions/:id, GET /v1/agent-sessions/:id/page-state, GET /v1/agent-sessions/:id/network, GET /v1/agent-sessions/:id/cookies, GET /v1/agent-sessions/:id/downloads and GET /v1/agent-sessions/:id/frames.
    • Controls and live video on agent_sessions:live: POST /v1/agent-sessions/:id/stop and POST /v1/agent-sessions/:id/livekit-token.
    • agent_sessions:live also carries the desktop app’s own live-window calls — switching mode, taking over and handing back, and its connection report — which are not part of the public API.

The dashboard pages you browse while signed in draw on a separate dashboard limit wherever an API call would draw on global, so browsing never uses your plan’s API allowance. It is 300, then 5 a second, or your plan’s global figures where those are larger. A global limit agreed for your account applies to it figure by figure: a figure below your plan’s global binds, a figure above it never gives less than the dashboard figures. Every answer to such a request, a 429 too, carries x-ratelimit-bucket: dashboard. It is not one of the bucket keys above, and GET /v1/account/rate-limits does not list it; GET /v1/account/limits does.

Each call uses only that named bucket key: a POST /v1/sessions uses sessions:create (never global), and a POST /v1/agent-sessions/:id/message uses agent_sessions:message, and a POST /v1/agent-sessions/:id/steps uses agent_sessions:steps, and a POST /v1/agent-sessions/:id/tabs uses agent_sessions:tabs.

The account scope depends on who owns the resource:

  • A self-scoped request consumes that bucket once for the caller.
  • A per-session GUI control key consumes it once for the session owner, at the owner’s own tier and active overrides — not a reduced desktop allowance. It draws on the same bucket as that account’s API traffic on the same route: a desktop session window left open (it polls page state roughly every 2s) is charged to agent_sessions:live, which your SDK’s reads of those session routes share, so size that bucket for the total. List calls stay on global, so they no longer compete with the window.
  • A session or agent-session request made with X-Driftstack-Account first consumes the actor’s bucket, then consumes the same bucket key and cost for the selected owner. This keeps every member accountable for their own traffic while all members share the owner’s session budget.

The second consume is scoped to the session and agent-session routes — the resources that hold a live browser. Other endpoints that honour X-Driftstack-Account (profiles, profile snapshots, webhooks and usage) currently charge the acting member only, so their owner-scoped reads and writes do not aggregate into one shared budget.

The owner consume uses the owner’s current tier and active staff override. Authorization and role checks happen before the owner bucket is read.

Per-tier defaults

Plan Tier id global capacity global refill (rps) sessions:create capacity sessions:create refill (rps) agent_sessions:message capacity agent_sessions:message refill (rps) agent_sessions:input_event capacity agent_sessions:input_event refill (rps) agent_sessions:live capacity agent_sessions:live refill (rps) agent_sessions:steps capacity agent_sessions:steps refill (rps) agent_sessions:tabs capacity agent_sessions:tabs refill (rps)
Trial trial 60 1 5 1/60 (1 per minute) 20 1/5 (12 per minute) 240 60 240 4 40 1/2 (30 per minute) 40 1/2 (30 per minute)
Starter starter_v3 120 2 10 1/30 (2 per minute) 40 1/3 (20 per minute) 360 90 240 4 40 1/2 (30 per minute) 40 1/2 (30 per minute)
Pro pro_v3 360 6 20 1/10 (6 per minute) 100 1 480 120 720 12 120 1.5 (90 per minute) 120 1.5 (90 per minute)
Team team_v3 900 15 40 1/5 (12 per minute) 200 2 540 135 1,440 24 240 3 240 3
Scale scale_v3 1,800 30 60 1 300 3 600 150 2,880 48 480 6 480 6
Enterprise enterprise Custom Custom Custom Custom Custom Custom Custom Custom Custom Custom Custom Custom Custom Custom

Enterprise buckets are set by contract.

The earlier plans, no longer offered, keep their buckets for the accounts that hold them:

Plan Tier id global capacity global refill (rps) sessions:create capacity sessions:create refill (rps) agent_sessions:message capacity agent_sessions:message refill (rps) agent_sessions:input_event capacity agent_sessions:input_event refill (rps) agent_sessions:live capacity agent_sessions:live refill (rps) agent_sessions:steps capacity agent_sessions:steps refill (rps) agent_sessions:tabs capacity agent_sessions:tabs refill (rps)
Personal solo_manual 120 2 10 1/30 (2 per minute) 40 1/3 (20 per minute) 360 90 240 4 40 1/2 (30 per minute) 40 1/2 (30 per minute)
Team team_manual 360 6 20 1/10 (6 per minute) 100 1 480 120 720 12 120 1.5 (90 per minute) 120 1.5 (90 per minute)
Agency agency_manual 1,800 30 60 1 300 3 600 150 1,920 32 320 4 320 4
API Starter api_starter 240 4 15 1/20 (3 per minute) 60 1/2 (30 per minute) 360 90 480 8 80 1 80 1
API Builder api_builder 1,800 30 60 1 300 3 600 150 1,920 32 320 4 320 4
API Scale api_scale 6,000 100 120 2 1,000 10 1,200 300 5,760 96 1,000 12 1,000 12

Capacity = max burst size before the next refill kicks in. Refill = sustained rate (tokens per second). Effective sustained RPS for a default-cost call is the refill column.

agent_sessions:live, agent_sessions:steps and agent_sessions:tabs are the plan figure, raised when your account’s session limit is raised: an account allowed more sessions at once than its plan includes gets those three buckets larger than the table, in proportion. The other buckets, global included, stay at the table’s figures.

agent_sessions:steps is 40, then 30 a minute, for each session your plan runs at once, and never less than your plan’s agent_sessions:message figures (on API Scale the message capacity, 1,000, is larger; on Enterprise both are). Tab calls have their own limit, agent_sessions:tabs, with the same figures as agent_sessions:steps.

What happens when you hit the cap

When the caller’s actor bucket is exhausted, the API returns HTTP 429 with an RFC 9457 problem-details body (application/problem+json):

{
  "type": "https://errors.driftstack.dev/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit for \"global\" exceeded for tier \"starter_v3\".",
  "retry_after_seconds": 12
}

The standard Retry-After HTTP header carries the same value as retry_after_seconds. SDK clients honour it automatically with exponential backoff capped at 10s. Two calls are the exception, and the SDKs never retry either for you: an agent-session message (message()) and the transcript reader (transcript()) — wait retry_after_seconds and call again yourself. A 429 on a message refuses it before the turn does any work, so the same Idempotency-Key still works. Idempotency keys lists the requests that accept one, how a key is matched, and which other refusals free it.

When a distinct selected owner’s bucket is exhausted, the request also returns 429, but the problem remains generic ("Rate limit exceeded."). The generic body retains retry_after_seconds; among rate-limit headers, only Retry-After remains. The owner’s tier, capacity, remaining tokens, override, and reset policy are not disclosed. The actor token was already consumed and is not refunded.

Per-account overrides

Driftstack support can configure per-account overrides. Customers reaching legitimate high-throughput workloads (Enterprise, agencies running scraping jobs across many domains) are bumped above the per-tier default on request. Email [email protected] with workload shape + expected steady-state RPS.

Lists of steps and tab calls once drew on agent_sessions:message. If your account has an agreed agent_sessions:message limit and none on agent_sessions:steps (or, for tab calls, none on agent_sessions:tabs), that limit carries over to your steps and tab calls figure by figure, compared with your plan’s agent_sessions:message figure: a figure below it binds them too; a figure above it never gives your steps and tab calls less than their own figure (they get the larger). This lasts until the agreed limit ends. An agreed agent_sessions:steps limit does not change your tab calls.

Reading your current cap

GET /v1/account/limits returns, in one read, each bucket’s plan figure, any agreed limit and the figure enforced now, and your session, saved-profile, restore and saved-proxy limits with what is in use.

GET /v1/account/rate-limits returns the effective per-bucket config for your account, including any overrides:

{
  "tier": "scale_v3",
  "buckets": [
    {
      "bucket_key": "global",
      "capacity": 1800,
      "refill_per_second": 30,
      "source": "tier_default",
      "override_expires_at": null
    },
    {
      "bucket_key": "sessions:create",
      "capacity": 60,
      "refill_per_second": 1,
      "source": "tier_default",
      "override_expires_at": null
    }
  ]
}

source is "tier_default" or "override"; when an override is active, override_expires_at carries the auto-revert timestamp. A "tier_default" row for agent_sessions:live, agent_sessions:steps or agent_sessions:tabs is the plan figure, raised when your account’s session limit is raised. An agent_sessions:steps or agent_sessions:tabs row whose figures an agreed agent_sessions:message limit changed (a lower figure that binds, or a higher one above the steps figure) shows "override", with that limit’s end in override_expires_at; one that leaves the steps figures as they are shows "tier_default". Full read-endpoint docs at /api/account-rate-limits.

The dashboard renders this data on app.driftstack.io/usage — the “Limits for your account” card lists each bucket’s capacity, refill rate, and source (tier default vs staff override, with the override’s auto-revert date). The endpoint remains available for SDK / curl reads.

Response headers

Allowed authenticated /v1/* responses and actor-bucket denials carry four x-ratelimit-* headers reflecting the actor bucket:

Header Meaning
x-ratelimit-bucket Which bucket the call drained — see the bucket keys above.
x-ratelimit-limit Bucket capacity (matches the capacity row from the read endpoint).
x-ratelimit-remaining Tokens left in the bucket after this call (integer, floor of fractional).
x-ratelimit-reset Unix-seconds timestamp when the bucket refills to full capacity.

For a distinct effective-owner denial, these policy headers are removed to avoid disclosing another account’s capacity. That response carries Retry-After only. For an actor-bucket 429, retry loops can combine x-ratelimit-remaining=0 with Retry-After to drive a back-off.

The IETF draft-standard names are emitted alongside the x- set, for gateways and generic client libraries that read the un-prefixed form:

Header Meaning
ratelimit-limit Same value as x-ratelimit-limit.
ratelimit-remaining Same value as x-ratelimit-remaining.
ratelimit-reset Seconds from now until full refill (relative, per the draft — NOT a timestamp).

On successful team requests, these values describe the actor bucket; the selected owner’s remaining budget is intentionally not exposed.

Note the one semantic difference: ratelimit-reset is relative delta-seconds, while x-ratelimit-reset is an absolute unix-seconds timestamp. Parse whichever form your tooling expects.

Requests per address, and per account

Before any call is signed in, it is counted against the address it comes from (for IPv6, the /64 network): 600 at once, then 10 a second. A call with a missing, wrong or revoked key stays counted there, and past that limit it gets 429 with x-ratelimit-bucket: global_ip.

Where per-account counting is switched on, a call that signs in is counted against your account instead, so many windows or scripts behind one office or server address share your account’s allowance rather than the address’s. On a paid plan the allowance is 600 calls a minute, plus your plan’s global requests a minute, plus 240 for each session your plan can run at once (960 a minute for Starter, 12,360 for API Scale). On the Trial it is 240 for each session. No account’s allowance is above 16,800 a minute (280 a second); Enterprise contracts that need more from one address should talk to us. The per-route buckets above still apply. While the allowance lasts, a signed-in call’s address count is given back, and the response carries x-ratelimit-bucket: ingress unless a per-route bucket above sets its own headers. When the allowance is used up, on any plan, nothing new refuses your calls: they count against the address again, as they always did, and past the address limit they get 429 with x-ratelimit-bucket: global_ip and a Retry-After. On the Trial, the calls of all Trial accounts on one address together get at most 1,200 a minute counted this way. If we have raised your account’s global limit, the allowance grows with it.

Sign-up: a daily limit per IP address

Creating an account happens before there is an API key, so the sign-up call (the one the dashboard’s sign-up form makes) is limited per IP address instead of per account (for IPv6, per /64 network). It has a short burst limit like the other sign-in calls, and on top of it a daily ceiling: at most 25 sign-up requests from one address in any rolling 24 hours. Every request the ceiling lets through counts, including one later refused for bad input or an email that is already registered. No other call has this per-address daily ceiling.

Every sign-up request that gets past the burst limit carries two extra headers, so a shared office network sees the ceiling coming before it is refused:

Header Meaning
x-ratelimit-daily-remaining Sign-up requests left for this address in the rolling 24 hours (integer).
x-ratelimit-daily-reset Unix-seconds timestamp 24 hours after the newest sign-up request from this address, when the window is empty again and all 25 are free.

A slot can free well before x-ratelimit-daily-reset: each request leaves the window 24 hours after it was made. When the ceiling is reached, sign-up answers 429 with Retry-After, the seconds until the oldest request in the window leaves it and frees a slot; wait for that, not for the reset time.

If the limit store cannot be reached, sign-up answers 429 with Retry-After: 60 and without the two daily headers.

Reporting a discrepancy

The limits above are the same ones the API enforces and the same ones shown in your dashboard. If you notice a mismatch, email [email protected].