Account rate limits
Driftstack enforces per-tier token-bucket rate
limits on every authenticated /v1/* call. The
/v1/account/rate-limits endpoint exposes the effective config
your account is hitting right now — tier defaults merged with any
active admin overrides.
For the broader explanation of how rate limits work + the per-tier defaults table, see /reference/rate-limits.
To read these limits at once — these buckets with your plan’s figure beside
the enforced one, plus your session, profile, restore and proxy limits with
what is in use — call GET /v1/account/limits.
Get effective rate-limit config
GET /v1/account/rate-limits
Returns the rate-limit config that’s actually being applied to
this account. Seven bucket keys exist: global (every
authenticated /v1/* call that doesn’t have a dedicated bucket
below), sessions:create
(POST /v1/sessions and POST /v1/profiles/:id/launch — lower cap
because session creation is expensive), agent_sessions:message
(POST /v1/agent-sessions/:id/message — separate cap so an
LLM-driven message loop can’t drain the global bucket),
agent_sessions:steps (POST /v1/agent-sessions/:id/steps, the lists of
steps you write — they run no AI, so they have their own cap, apart from
messages; it grows with the number of sessions your plan runs at once),
agent_sessions:tabs (POST /v1/agent-sessions/:id/tabs, the tab calls —
their own cap with the same figures as agent_sessions:steps),
agent_sessions:input_event (the desktop app’s live input into a
session a person is driving — separate cap so input streams don’t drain
the global bucket), and agent_sessions:live (what a live window reads
and controls on one agent session — separate so list calls on global
can’t slow a session you are watching; it grows with the number of
sessions your plan runs at once).
Response (200):
{
"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
},
{
"bucket_key": "agent_sessions:message",
"capacity": 300,
"refill_per_second": 3,
"source": "tier_default",
"override_expires_at": null
},
{
"bucket_key": "agent_sessions:input_event",
"capacity": 600,
"refill_per_second": 150,
"source": "tier_default",
"override_expires_at": null
},
{
"bucket_key": "agent_sessions:live",
"capacity": 2880,
"refill_per_second": 48,
"source": "tier_default",
"override_expires_at": null
},
{
"bucket_key": "agent_sessions:steps",
"capacity": 480,
"refill_per_second": 6,
"source": "tier_default",
"override_expires_at": null
},
{
"bucket_key": "agent_sessions:tabs",
"capacity": 480,
"refill_per_second": 6,
"source": "tier_default",
"override_expires_at": null
}
]
}
When an active admin override is in place for a bucket, that row
shows source: "override" plus the override’s override_expires_at
timestamp:
{
"bucket_key": "global",
"capacity": 6000,
"refill_per_second": 100,
"source": "override",
"override_expires_at": "2026-06-15T12:00:00Z"
}
After the override expires, subsequent reads return the tier-default row again.
Lists of steps and tab calls once drew on agent_sessions:message, so an agreed
agent_sessions:message limit carries over to them: while your account has
one and none on agent_sessions:steps (for tab calls, none on
agent_sessions:tabs), each agent_sessions:steps and agent_sessions:tabs
figure is compared with your plan’s agent_sessions:message figure. A figure
below it binds your steps and tab calls too; a figure above it never gives
less than the steps figure (the larger applies). When the result differs
from your steps figures, the row shows source: "override" and that limit’s
override_expires_at. An agreed agent_sessions:steps limit does not
change the agent_sessions:tabs row.
Requires the broad read scope; account_owner also satisfies the
gate. Resource-granular, write-only, and zero-scope keys cannot inspect
account-wide limits or staff-applied override metadata.
Bucket reference
| Bucket key | Consumed by | Why a separate bucket? |
|---|---|---|
global |
Every authenticated /v1/* without a dedicated bucket |
Coarse anti-abuse cap — protects against runaway scripts |
sessions:create |
POST /v1/sessions and POST /v1/profiles/:id/launch |
Lower cap because session creation starts a browser; a profile launch creates a session too, and so does starting or reopening an agent session (POST /v1/agent-sessions, POST /v1/agent-sessions/:id/reopen) |
agent_sessions:message |
POST /v1/agent-sessions/:id/message |
Isolated from global so an LLM-driven message loop can’t drain the global cap |
agent_sessions:steps |
POST /v1/agent-sessions/:id/steps |
A list of steps runs no AI, so it does not share the message cap; grows with the sessions your plan runs at once |
agent_sessions:tabs |
POST /v1/agent-sessions/:id/tabs |
A tab call runs no AI, so it does not share the message cap; its own limit with the same figures as agent_sessions:steps, so tab calls and lists of steps do not use up each other |
agent_sessions:live |
A live window’s calls on one agent session, see below | Isolated so list calls on global can’t slow a session you are watching; grows with the sessions your plan runs at once |
agent_sessions:input_event |
The desktop app’s live input | High-frequency live input — isolated so input streams can’t drain the global cap; delivery to the session has its own lower limit |
On your own account each API call drains exactly one bucket in this table
(the signed-in dashboard draws on its own dashboard limit instead of
global; see Rate limits). A call you make for a team owner on a session or agent-session route (with
X-Driftstack-Account, or on a session the owner owns) drains two: yours,
then the owner’s bucket for the same route (see
Team roles). A POST /v1/sessions consumes
only from sessions:create — it never touches global — and
hitting that bucket’s cap returns 429.
A session-create call counts against sessions:create whether or not a
session starts: one refused for a missing proxy, for no free device or by the
concurrency limit has still used one.
The 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.
The 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. Listing agent sessions
(GET /v1/agent-sessions) stays on global.
Per-tier defaults
The defaults the endpoint returns when no override is active are in the full table at /reference/rate-limits.
For agent_sessions:live, agent_sessions:steps and agent_sessions:tabs,
a "tier_default" row is the plan figure, raised when your account’s session
limit is raised: an account allowed more sessions at once than its plan
includes sees those three buckets larger than the table, still with source: "tier_default". Every
other bucket, global included, stays at the table’s figure.
Higher limits
Customers needing legitimate high-throughput workloads (Enterprise,
agencies running scraping jobs across many domains) can ask
[email protected] for higher limits, with the workload shape and
the expected steady-state requests per second. A raised limit shows up
here with source: "override" and the date it reverts to the tier
default in override_expires_at.
Customer-dashboard surface
The /usage page on the dashboard renders this endpoint’s data in
its “Limits for your account” card — per-bucket capacity, refill rate, and
source (tier default vs raised limit, with the date it reverts). The endpoint remains available for SDK / curl
reads.
Response headers
A /v1/* response carries four x-ratelimit-* headers for one bucket
the call drew from (on a call made for a team owner, yours, never the
owner’s). A call that reaches its route’s bucket in the
table above (or dashboard, for the signed-in dashboard) gets that
bucket’s figures, whether it then succeeds, fails or is refused with 429. A call refused before that point gets the
figures of the per-address global_ip bucket: a 401 always does, and
so does a signed-in call refused early, such as a 403 for a missing
scope, unless per-account counting is on and your account’s allowance
took the call, in which case it gets your account’s ingress figures
(see
Requests per address, and per account).
A webhook test or replay also draws on a fixed webhooks:send bucket,
and a 429 from that bucket carries its own figures
(x-ratelimit-bucket: webhooks:send; see
Webhook endpoints). When a team owner’s own
bucket refuses a call you made for that owner, the 429 carries only
Retry-After and no x-ratelimit-* headers. Check x-ratelimit-bucket
before you use the other values, and do not require the headers.
| Header | Meaning |
|---|---|
x-ratelimit-bucket |
Which bucket the call drained — see the bucket reference above. |
x-ratelimit-limit |
Bucket capacity — same as capacity from this 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. |
A POST /v1/sessions consumes only from sessions:create — the
headers name that bucket in x-ratelimit-bucket because the call drains
no other bucket key.
These headers are also surfaced on 429 responses; combine
x-ratelimit-remaining=0 + Retry-After to drive a back-off
without an extra round-trip.
The IETF draft-standard ratelimit-limit / ratelimit-remaining /
ratelimit-reset names are emitted alongside the x- set. One
semantic difference: ratelimit-reset is relative seconds-from-now
(per the draft), while x-ratelimit-reset is an absolute unix
timestamp. Full table at
/reference/rate-limits.
What happens when you hit a cap
The API returns HTTP 429 with an RFC 9457 problem-details body:
{
"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 detail names which bucket and tier were hit. To branch on the
exact bucket programmatically, read the x-ratelimit-bucket response
header (e.g. global) — it is a header, not a body field, and is
sent on success responses too, not just 429s (see Response headers
above for which bucket it names).
The Retry-After HTTP header carries the same value as
retry_after_seconds. SDK clients honour it automatically with
exponential backoff capped at 10s; see
/reference/errors for retry-loop examples.