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/sessionsandPOST /v1/profiles/:id/launch. Starting or reopening an agent session draws on it too:POST /v1/agent-sessionsandPOST /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 fromglobalso 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 asagent_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 theglobalcap. 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 onglobalcan’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 onglobal.- 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/downloadsandGET /v1/agent-sessions/:id/frames. - Controls and live video on
agent_sessions:live:POST /v1/agent-sessions/:id/stopandPOST /v1/agent-sessions/:id/livekit-token. agent_sessions:livealso 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.
- Reads on
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 onglobal, so they no longer compete with the window. - A session or agent-session request made with
X-Driftstack-Accountfirst 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].