Drıftstack docs
Docs

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.