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:
| Tier | Requests / sec | One-minute bucket | Short burst bucket | Signals reads / sec† | Human pageview cap* | Signals Coverage | Overage behavior |
|---|---|---|---|---|---|---|---|
| Hobby | 100 | 6,000 requests | 12,000 requests | 10 | 50,000 pageviews | 250,000 events | Stops at included allowance; no paid overage |
| Growth | 1,000 | 60,000 requests | 120,000 requests | 100 | 500,000 pageviews | 2.5M events | Stops at included unless overages are enabled |
| Scale | 5,000 | 300,000 requests | 600,000 requests | 5,000 | 5,000,000 pageviews | 25M events | Stops at included unless overages are enabled |
| Network | 25,000 | 1,500,000 requests | 3,000,000 requests | 25,000 | 25,000,000 pageviews | 250M events | Stops at included unless overages are enabled |
| Enterprise | Contract | Contract | Contract | Contract | Contract | Contract | Negotiated |
† 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:
| Tier | Materialized identity signal values | Graph queries |
|---|---|---|
| Hobby | Unavailable | Unavailable |
| Growth | 2,500,000 | Unavailable |
| Scale | 25,000,000 | 100,000 |
| Network | 250,000,000 | 1,000,000 |
| Enterprise | 250,000,000 | 1,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:
| Tool | Effective ceiling |
|---|---|
list_segments | 12 calls / minute / user |
list_people and count_people, when the call has to freeze a cohort | 30 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
| Header | Meaning |
|---|---|
X-ClickStream-Billing-Period | YYYY-MM the counters belong to. |
X-ClickStream-Monthly-Limit | Included human-pageview allowance for the plan. |
X-ClickStream-Monthly-Used | Human pageviews consumed so far this period. |
X-ClickStream-Billing-Mode | full (inside the included allowance), metered_overage (past it, overages on), or blocked. |
X-ClickStream-Budget-Ceiling | The 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:
- The five billing headers appear only when the request contained at least one non-bot
pageview. Send a batch of clicks and you get none of them. - The five coverage headers appear only when the request contained at least one bot-classed event.
- A single human
customevent therefore returns a202with noX-ClickStream-*headers at all. If you are testing the header contract, send apageview.
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 id — builder 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
POST /v1/events— the main collector counter. The limiter counts the HTTP request, not the events inside it, so a 25-event batch costs one unit of rate limit. The monthly human billing cap counts acceptedpageviewevents that are not clearly bot-classed; clearly non-human pageviews count toward Signals Coverage instead. Body and batch caps are on the Event schema page.POST /v1/edge/pageview— the only route on/v1/edge. There is no/v1/edge/capture; that path 404s. Counts toward Signals Coverage for crawler, answer-engine, preview, monitor, and automation capture. Normal browser traffic should stay on the browser SDK. Three things bite first-time callers:- Production requires
requestIdortimestampin the JSON body. Send neither and you get400 idempotency_required— a request with onlyurlanduserAgentfails on the first call. - The default
modeisbots_only. Human-looking traffic returns202 { "success": true, "accepted": 0, "reason": "browser_sdk_preferred" }— accepted-looking, stored nothing. Send"mode": "all"if you actually want human hits from this route. - The gate is the
urlhostname, not anOriginheader. A captured URL whose hostname matches no configured domain returns403 domain_not_allowed. This route is not in the browser-provenance set, so a Worker can call it with noOrigin.
- Production requires
GET /v1/signals/:visitorId— uses a separatesignals-readbucket so page decisions do not consume event-write capacity, and that bucket is clamped to 10/sec on Hobby and 100/sec on Growth (see the tier table). The official client sendssessionIdso the edge reads one live session partition. Hobby keys must includesessionIdor the call returns400 session_id_required. Paid keys may omit it, but then the read fans out across all 10 live-session partitions and bills 10 Signals Coverage units instead of 1 — always sendsessionId. Keep Hobby/Growth usage to one-shot or coarse cached checks; sustained polling belongs on Scale+.GET /v1/signals/:visitorId/streamWebSocket — browser/page-code realtime stream. Scale+ only (403 plan_upgrade_requiredbelow that, decided by the matched property's plan), requiressessionId(400 session_id_required), opens exactly one live-session partition, reserves 300 Signals Coverage units on subscribe, caps at 10 visitor streams per partition, and closes after five minutes or two idle minutes. Exhausted coverage returns402 signals_coverage_limit_reached. Clients fall back to polling when capped.GET /signals/streamWebSocket — private tenant feed for dashboard/server subscribers. This is separate from browser page-code realtime and still requires a short-lived stream token; a public site key is not accepted.- Optional enrichment — managed enrichment runs server-side inside the collector when an event carries a hashed email and the property has it enabled. There is no customer-callable enrichment endpoint and no per-lookup fee, so it consumes none of the request limits on this page. It is gated by plan and per-property settings.
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
- API keys + auth — key types, permission scopes, rotation, error codes
- Event schema — what each accepted event actually records
- Pricing — tiers, meters, included quotas