Rate limits

ClickStream rate-limits per account, per key type, per scope — the bucket is clientId:keyType:scope, not the key itself. The public tier number is the sustained collector requests per second that bucket allows. The SDK batches events, so one request can carry more than one click, scroll, or identify event.

Behind the scenes, the collector enforces that number in one-minute windows with a generous 2x burst allowance for real-world spikes. High-throughput keys are deterministically spread across fixed-capacity limiter partitions whose quotas add up to the published key-wide allowance; this avoids routing a 25,000-request/second key through one single-threaded coordinator. In plain English: you can spike briefly without getting cut off, but sustained traffic above your tier eventually receives 429 responses until the bucket resets.

Per-tier caps

Collector caps, per bucket, measured as sustained requests/second:

TierRequests / secOne-minute bucketShort burst bucketSignals reads / sec†Human pageview cap*Signals CoverageOverage behavior
Hobby1006,000 requests12,000 requests1050,000 pageviews250,000 eventsStops at included allowance; no paid overage
Growth1,00060,000 requests120,000 requests100500,000 pageviews2.5M eventsStops at included unless overages are enabled
Scale5,000300,000 requests600,000 requests5,0005,000,000 pageviews25M eventsStops at included unless overages are enabled
Network25,0001,500,000 requests3,000,000 requests25,00025,000,000 pageviews250M eventsStops at included unless overages are enabled
EnterpriseContractContractContractContractContractContractNegotiated

GET /v1/signals/* runs in its own signals-read bucket, and on the two lower tiers that bucket is clamped far below the headline number: Hobby is capped at 10 reads/sec and Growth at 100 reads/sec no matter what the requests/sec column says. Scale and above use the tier's normal rate. The clamp is decided by the matched property's plan, not by the key's plan — a key shared across a Hobby property and a Scale property gets 10/sec on the Hobby one.

Dedicated mobile and server keys have their own, lower ceiling

cs_mob_live_* and cs_srv_live_* keys skip the browser Origin/Referer gate, so ClickStream replaces that protection with a tighter per-key throughput ceiling. The dashboard mints them at half the plan's requests/second, with a floor of 50 — Hobby 50/sec, Growth 500/sec, Scale 2,500/sec, Network 12,500/sec — and the collector applies whichever is lower, that ceiling or the scope rate above. A hand-created key config with no explicit rate limit falls back to 50/sec.

Mobile/server keys also count in a separate bucket from website keys under the same account, so a native app cannot exhaust your web ingest allowance (or the reverse).

The bucket is keyed by account, key type and scope — not by key — and that cuts both ways. Two Mobile-app properties under one account hold two distinct cs_mob_live_* keys, and those two keys share one mobile:ingest counter. Minting a second property key does not buy a second allowance; only a different key type or a different scope does.

*The human pageview cap is the included quota for billable human pageviews. Bot, AI crawler, answer-engine, automation, kiosk, and tool traffic does not count as human pageviews, but it can count toward Signals Coverage because it still requires traffic-intelligence work. Click, scroll, form, custom, and identify events help classification but do not count as human pageviews. See the Pricing page for the full tier matrix.

The Signals-event allowance is also the finite accepted non-pageview event safety ceiling for click, scroll, form, custom, and identify receipts. Admission is atomic: denied batches and duplicate event IDs consume no monthly units. This is a storage/compute guard and does not create a separate billable meter. With paid overages enabled, the default absolute guard is 2× the included event allowance unless a reviewed contract sets another ceiling.

Hitting that guard is a distinct error from the pageview budget: 429 accepted_event_safety_ceiling_reached (the pageview stop is 429 usage_limit_exceeded), with Retry-After: 3600 and a X-ClickStream-Budget-Type: accepted-events header. Branch on the error field in the body — that header is not in the CORS expose list, so cross-origin browser JavaScript cannot read it.

Exports also have cumulative calendar-month resource budgets. Rows default to the plan Signals Coverage allowance, bytes reserve 2,048 bytes per row, and compute reserves three record-work units per row. Jobs reserve before a scan/provider call and settle actual usage. Growth allows 100 export jobs and 10 audience builds per month; Scale allows 500 jobs and 30 audience builds; Network allows 2,500 jobs and 100 audience builds. At most three audience builds may be unpaid or in flight at once.

Direct identity API allowances

These allowances apply only to direct tenant-key calls to the identity service — a separate surface from the collector endpoints above. It is not self-serve: no dashboard flow mints an identity-service tenant key, and the service's request and response shapes are not documented here. If you do not have a tenant key, none of this section applies to you; ClickStream's own identity resolution runs behind the collector and needs no call from you.

The allowances are finite, purchaser-wide, and reset on the calendar month. They are operational ceilings, not Stripe meters:

TierMaterialized identity signal valuesGraph queries
HobbyUnavailableUnavailable
Growth2,500,000Unavailable
Scale25,000,000100,000
Network250,000,0001,000,000
Enterprise250,000,0001,000,000

These are hard operational safety ceilings in the current runtime. A bespoke commercial agreement does not silently change them; any higher ceiling requires a reviewed runtime release and fresh capacity/profitability evidence first.

POST /v1/signals and POST /v1/signals/batch count actual materialized identity values, not request envelopes or metadata. GET /v1/graph/:nodeId and its edge reads reserve one query before graph work, including not-found lookups, so random identifiers cannot create an unbounded database-read lane. Duplicate or denied pre-work requests do not consume signal units. Missing, stale, or ambiguous purchaser scope and unavailable metering fail closed.

MCP is independently bounded at 120 requests per token per minute. All MCP tokens and team members share 1,000,000 requests per purchaser per fixed 31-day window, while credential issuance has a separate 100-mint purchaser allowance per fixed 31-day window. There is no token, tool, workflow, agent, or integration-count cap.

The 120/minute ceiling is rarely the one you hit first. Two MCP tools wrap inner dashboard limits that are an order of magnitude tighter, counted per user and shared across every token that user holds:

ToolEffective ceiling
list_segments12 calls / minute / user
list_people and count_people, when the call has to freeze a cohort30 calls / minute / user

These do not arrive as an HTTP 429 with Retry-After. The MCP endpoint answers 200 with an error tool result, so backoff logic written against status codes will never trigger. Back off on the tool result, not the status code.

Tracking-domain lifecycle operations are also purchaser-wide and non-billable. Each fixed 31-day window allows 10 hostname claims, 5 Cloudflare provisions, 100 status checks, and 20 deletions. Hourly burst ceilings are 3, 2, 10, and 5 respectively. Exact same-host reads and completed-host retries are idempotent; they do not re-provision Cloudflare. Replacement keeps the current hostname live until the requested hostname is active, then retires the old Cloudflare and KV resources through a durable cleanup job.

Why request limits, human pageviews, and Signals Coverage are separate

Request limits protect the collection pipe from floods. Human pageview caps are the primary value meter. Signals Coverage protects the infrastructure cost of classifying non-human traffic and serving active Signals decisions.

That means a normal SDK batch with one human pageview plus several clicks still uses one collector request, while only the human pageview counts toward the human monthly cap. Non-human crawler or tool traffic can count toward Signals Coverage instead. Browser visitor realtime streams also draw from Signals Coverage: each open stream reserves 300 units, matching the five-minute server cap at 1Hz active Signals reads. This keeps Hobby useful for real sites and keeps paid customers from being cut off too early during normal interaction-heavy sessions.

Burst allowance

The effective short-burst bucket is 2x the sustained one-minute bucket. A Growth key has a sustained 60,000-request minute and a short burst ceiling around 120,000 requests before the limiter starts rejecting. The sustained 1,000 requests/sec rate reasserts as the rolling window advances.

This exists because real-world traffic is not smooth: brief traffic spikes, crawler waves, monitoring bursts, and QA runs can raise request volume for a few seconds. The burst bucket absorbs the first spike; sustained load beyond the tier still hits the cap.

429 response

When the bucket is exhausted, the collector returns:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 47
X-RateLimit-Limit: 60000
X-RateLimit-Burst-Limit: 120000
X-RateLimit-Policy: 60000;w=60;burst=2
X-RateLimit-Scope: ingest
X-RateLimit-Unit: requests
X-RateLimit-Remaining: 0
X-RateLimit-Burst-Remaining: 0
X-RateLimit-Reset: 1713797700

{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded (ingest: 1000 requests/sec sustained, 2x burst). Retry in 47s.",
  "retryAfter": 47
}

Well-behaved clients, including the ClickStream SDK, respect Retry-After and back off. If you are building your own sender, keep a small local queue and retry after the reset instead of dropping data immediately.

Headers on successful responses

Every authenticated response, not just 429, carries the rate-limit state so you can budget preemptively:

X-RateLimit-Limit: 60000
X-RateLimit-Burst-Limit: 120000
X-RateLimit-Policy: 60000;w=60;burst=2
X-RateLimit-Scope: ingest
X-RateLimit-Unit: requests
X-RateLimit-Remaining: 119847
X-RateLimit-Burst-Remaining: 119847
X-RateLimit-Reset: 1713797700   # unix epoch seconds

Use X-RateLimit-Scope to see which bucket you are using (ingest, signals-read, or api). Use X-RateLimit-Limit and X-RateLimit-Burst-Limit when you are writing your own sender and need the exact one-minute bucket size.

X-RateLimit-Remaining is exact for low-volume keys that use one limiter partition. For horizontally partitioned high-throughput keys it is a bounded aggregate estimate derived from the request's partition; the limit, burst limit, reset time, and 429 admission decision remain authoritative. Do not use the remaining header as an accounting ledger.

Usage headers on successful responses

Rate limits are not the only budget you can run out of. Successful POST /v1/events and POST /v1/edge/pageview responses also carry your monthly usage state, so you can watch your allowance drain without ever opening the dashboard:

HTTP/1.1 202 Accepted
X-ClickStream-Billing-Period: 2026-04
X-ClickStream-Monthly-Limit: 500000
X-ClickStream-Monthly-Used: 18412
X-ClickStream-Billing-Mode: full
X-ClickStream-Budget-Ceiling: 500000
HeaderMeaning
X-ClickStream-Billing-PeriodYYYY-MM the counters belong to.
X-ClickStream-Monthly-LimitIncluded human-pageview allowance for the plan.
X-ClickStream-Monthly-UsedHuman pageviews consumed so far this period.
X-ClickStream-Billing-Modefull (inside the included allowance), metered_overage (past it, overages on), or blocked.
X-ClickStream-Budget-CeilingThe hard stop. Equals the included allowance with overages off; 2× it by default with overages on.

Bot-classed traffic reports against Signals Coverage instead, with a parallel set:

X-ClickStream-Signals-Coverage-Period: 2026-04
X-ClickStream-Signals-Coverage-Limit: 2500000
X-ClickStream-Signals-Coverage-Used: 91044
X-ClickStream-Signals-Coverage-Ceiling: 2500000
X-ClickStream-Signals-Coverage-Mode: full

GET /v1/signals/:visitorId/stream adds X-ClickStream-Signals-Coverage-Reserved-Units on a successful open, reporting the units the stream reserved up front.

These headers are conditional, and their absence is not an error:

All eleven headers, plus Retry-After and the X-RateLimit-* set, are listed in Access-Control-Expose-Headers, so cross-origin browser JavaScript can read them directly:

const res = await fetch(`${endpoint}/v1/events`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
  body: JSON.stringify(pageviewEvent),
});

const used = res.headers.get('X-ClickStream-Monthly-Used');
const limit = res.headers.get('X-ClickStream-Monthly-Limit');
if (used && limit && Number(used) / Number(limit) > 0.9) {
  // 90% of the monthly allowance is gone — warn before the 429 arrives.
}

Monthly pageview and Signals budgets

Monthly human pageview caps are included allowances and collector admission budgets. With overage billing off, POST /v1/events and POST /v1/edge/pageview reject a request batch before storage when accepting it would exceed the allowance:

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-ClickStream-Billing-Mode: blocked
X-ClickStream-Budget-Ceiling: 500000
{
  "error": "usage_limit_exceeded",
  "reason": "included_limit_reached",
  "upgradeRequired": "builder",
  "billingPeriod": "2026-04",
  "used": 500000,
  "included": 500000,
  "safetyCeiling": 500000,
  "attempted": 3,
  "accepted": 0
}

reason is included_limit_reached or safety_ceiling_reached. upgradeRequired is an internal tier idbuilder is the tier the Pricing page calls Growth (see the id mapping).

Treat this 429 as a billing state, not an availability state. It is the one 429 where honouring Retry-After is pointless: retrying for an hour will not change the answer until the period rolls over, overage billing is enabled, or the plan changes. Senders that lump it in with rate_limit_exceeded will burn their retry budget and surface a bogus "collector unavailable" error to your team.

A batch is never partially admitted. If usage counters are unavailable, the collector returns retryable 503 usage_metering_unavailable before acceptance rather than creating an unaccounted spend path.

Non-pageview event storage/compute safety is aggregated by the site's actual purchasing account. Personal properties use the owner user; self-billing org properties use the organization, including properties owned by different users. The matched property's plan, billing cycle, and overage setting govern admission. A billing-resolved property never falls back to a mixed API-key allowance when its purchasing identity is missing.

Stripe overage billing is not enabled by default. Paid Stripe-backed accounts can opt into metered overages from the Billing page. Accepted overage traffic is marked metered_overage and is still bounded by a per-tenant safety ceiling; the default is 2x the included allowance unless the account has an explicit contract ceiling. Hobby has no paid overage path and stops at 50,000 monthly human pageviews.

Paid overages bill in blocks, not partial event dust: human pageview overage is $8.00 / 100k on Growth, $6.00 / 100k on Scale, and $0.50 / 100k on Network. Signals Coverage overage is ceil-billed per 1M coverage units at $16.00 on Growth, $5.00 on Scale, and $2.00 on Network.

When Signals Coverage is exhausted without paid overage, an opted-in account reaches its safety ceiling, or coverage metering is unavailable, customer websites keep working — but the degraded response is an HTTP 200, not an error, so code that only checks the status code will silently personalize on placeholder data.

GET /v1/signals/:visitorId returns 200 with coverageMode: "degraded", X-ClickStream-Signals-Coverage-Mode: degraded, and a neutral initializing snapshot (behavioralClass: "suspicious", no scores). Branch on coverageMode before you act on a snapshot:

const visitor = await res.json();
if (visitor.coverageMode === 'degraded') {
  // Placeholder data — fall back to your default experience, do not personalize.
}

The realtime stream is the exception: it refuses rather than degrades, with 402 signals_coverage_limit_reached.

Sites consistently above 1M daily visitors, or sites expecting replay/realtime Signals at 20M+ daily visitors, should move to Enterprise before launch. Self-serve Network is bounded to protect shared infrastructure; Enterprise contracts set explicit throughput, retention, realtime fan-out, and cost controls.

The dashboard recommends an upgrade only when a same-cycle projection at current usage—including tier-sensitive pageview, Signals Coverage, and installed-site charges on both tiers—shows the next tier is no more expensive. Shared same-price add-ons are excluded from both sides. Within the default 2x lower-tier safety ceiling, the lower tier can still be cheaper; moving up is then a capacity or feature decision, not a promised savings.

Per-endpoint notes

WAF layer

The request limiter described above is the application-layer limiter. Cloudflare's edge can still drop obvious abuse before it reaches a Worker invocation when zone-level protection is enabled.

See also