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.
Package caps
Collector caps, per bucket, measured as sustained requests/second:
| Package | Requests / sec | One-minute bucket | Short burst bucket | Signals reads / sec† | Human pageview cap* | Signals Coverage | Overage behavior |
|---|---|---|---|---|---|---|---|
| Explore | 100 | 6,000 requests | 12,000 requests | 10 | 5,000 pageviews | 25,000 events | Stops at included allowance; no usage charges |
| Pro | 5,000 | 300,000 requests | 600,000 requests | 5,000 | 25,000 pageviews | 250,000 events | Automatic usage billing above included allowances |
† GET /v1/signals/* runs in its own signals-read bucket. Explore is capped at 10 reads/sec; Pro can use up to 5,000 reads/sec subject to the account and site allowance. The clamp is decided by the matched property, not by the key label.
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 package's requests/second, with a floor of 50 — Explore 50/sec, Pro 2,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 units 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 package matrix.
Package allowances are pooled by billing account across its owner-billed sites; they are not multiplied by the number of installed sites. Billing displays an exact per-site attribution beneath the pooled totals. Shared sites and sites paid by a self-billing organization stay outside the owner's pool.
The Signals-event allowance is the included accepted non-pageview event budget for click, scroll, form, custom, and identify receipts. Admission is atomic: denied batches and duplicate event IDs consume no monthly units. This does not create a separate invoice meter: accepted events feed the existing Signals Coverage meter. With verified Pro metering, ingestion stays open to a high finite platform backstop unless a reviewed contract or monthly spend cap sets a lower stopping point.
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 package Signals Coverage allowance, bytes reserve 2,048 bytes per row, and compute reserves three record-work units per row. Explore includes bounded People CSV exports; Pro raises export allowances and bills usage above the included limits. Legacy provider-audience builds, where enabled for an existing account, remain subject to their own safety budget.
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:
| Package | Materialized identity signal values | Graph queries |
|---|---|---|
| Explore | Bounded preview allowance | Bounded preview allowance |
| Pro | Higher allowance, metered above included usage | Higher allowance, metered above included usage |
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 units instead. Browser visitor realtime streams also draw from Signals units: each open stream reserves 300 units, matching the five-minute server cap at 1Hz active Signals reads.
Burst allowance
The effective short-burst bucket is 2x the sustained one-minute bucket. A Pro key has a sustained 300,000-request minute and a short burst ceiling around 600,000 requests before the limiter starts rejecting. The sustained 5,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 package 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: 300000
X-RateLimit-Burst-Limit: 600000
X-RateLimit-Policy: 300000;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: 5000 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: 25000
X-ClickStream-Monthly-Used: 18412
X-ClickStream-Billing-Mode: full
X-ClickStream-Budget-Ceiling: 1000000000000
| 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; metered Pro uses a high finite platform backstop unless a lower contract or spend cap applies. |
Bot-classed traffic reports against Signals Coverage instead, with a parallel set:
X-ClickStream-Signals-Coverage-Period: 2026-04
X-ClickStream-Signals-Coverage-Limit: 250000
X-ClickStream-Signals-Coverage-Used: 91044
X-ClickStream-Signals-Coverage-Ceiling: 1000000000000
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: 25000
{
"error": "usage_limit_exceeded",
"reason": "included_limit_reached",
"upgradeRequired": "paid",
"billingPeriod": "2026-04",
"used": 25000,
"included": 25000,
"safetyCeiling": 25000,
"attempted": 3,
"accepted": 0
}
reason is included_limit_reached or safety_ceiling_reached. upgradeRequired
is the package that raises the allowance (paid for the higher self-serve
package).
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. Pro 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 high finite platform
backstop unless the account has an explicit contract ceiling or a lower
monthly variable-usage cap. Explore has no paid overage path and stops at 5,000
monthly human pageviews.
Pro overages bill in blocks, not partial event dust: human pageview overage is $50.00 / 100k and Signals Coverage overage is ceil-billed per 1M coverage units at $170.00. Explore traffic stops at its included allowance.
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 contact ClickStream before launch for a reviewed capacity contract with 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.
Pro overages are $50 per additional 100,000 human pageviews and $170 per additional 1M Signals units. The site, enrichment-enabled-site, and audience rates are listed on Pricing. DataMoon/DataShopper resolution calls are internal telemetry only and never generate a per-call Stripe charge. New Pro subscriptions use postpaid usage billing with a $25 interim invoice threshold; there is no prepaid wallet or credit balance.
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. Explore is clamped to 10/sec; Pro uses the higher package rate. The official client sendssessionIdso the edge reads one live session partition. Explore keys must includesessionIdor the call returns400 session_id_required. Pro keys may omit it, but then the read fans out across all 10 live-session partitions and uses 10 Signals Coverage units instead of 1 — always sendsessionId.GET /v1/signals/:visitorId/streamWebSocket — browser/page-code realtime stream available within the matched property's package allowance. It 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 controlled by per-property settings and compliance policy; Explore may use a bounded preview and Pro raises capacity.
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 — packages, meters, included quotas