Limits & quotas
Every limit Crossdeck enforces, in one place: per-endpoint API rate limits, request and payload caps, the bounds each SDK applies before an event ever leaves the device, and the error-capture caps that stop a crash loop from flooding your project. Defaults are generous enough that most projects never see a 429. If you need more, per-project overrides exist — contact support with the endpoint and the rate you need.
API rate limits
Rate limits are token buckets, scoped per project, per environment, per endpoint. The bucket refills linearly at the sustained rate and holds at most the burst capacity. Each request spends tokens equal to its cost; a request that the bucket can't cover is throttled. Burst capacity accrues while you're quiet — a project idle overnight can spend its full 1,000-event burst in a single batch, as long as the sustained average stays under the rate.
| Endpoint | Sustained rate | Burst capacity | Token cost |
|---|---|---|---|
POST /v1/events | 100 / s | 1,000 | Batch size — 1 event = 1 token |
POST /v1/purchases | 10 / s | 100 | 1 per request |
POST /v1/entitlements | 100 / s | 1,000 | 1 per request |
POST /v1/heartbeat | 1 / s | 10 | 1 per request |
POST /v1/identity/forget | 1 / s | 10 | 1 per request |
POST /v1/alias | 10 / s | 100 | 1 per request |
One additional velocity check sits on top of the /v1/alias bucket: more than 30 identify() calls for the same userId within 60 seconds returns 429 with retryAfterMs: 60000. It defends against an identify loop hammering one user record; the per-endpoint bucket above still applies independently.
If the rate-limit store is unreachable, requests are allowed, never dropped — Crossdeck's infrastructure misbehaving is not your problem. Defaults are per-endpoint; per-project overrides are stored server-side and raised through support.
Request & payload caps
Independent of rate, each request body is bounded. Two enforcement modes: reject fails the whole request with 400 and the index of the first bad event; soft-drop strips the offending field (or entry) and ingests the rest, reporting what was dropped.
| Surface | Limit | On violation |
|---|---|---|
/v1/events — batch size | 1–100 events per request | Reject (400) |
/v1/events — properties | 8 KB serialized JSON per event | Reject (400) |
/v1/events — name | 1–128 chars, A–Z a–z 0–9 _ . - : | Reject (400) |
/v1/events — eventId | 1–64 chars, A–Z a–z 0–9 _ - | Reject (400) |
/v1/events — identity hints | At least one of developerUserId (1–256 chars), anonymousId (1–128 chars), crossdeckCustomerId (cdcust_…) per event; multiple allowed, each present hint validated | Reject (400) |
/v1/events — tags | 32 keys per event; keys ≤ 64 chars (a–z 0–9 _ -); values ≤ 64 chars | Soft-drop bad entries |
/v1/events — categoryTags | 16 entries per event; ≤ 32 chars each; lowercased and deduplicated on ingest | Soft-drop bad entries |
/v1/alias — traits | 32 top-level keys; keys ≤ 64 chars; values primitive only — strings over 1,024 chars truncated, nested objects and arrays dropped | Soft-drop / truncate |
/v1/migration — user import | 1,000 rows per request | Reject (400) |
/v1/releases/sourcemaps — files | 100 source maps per request | Reject (400) |
/v1/releases/sourcemaps — map size | 10 MB per .map file | Per-file error; siblings still upload |
SDK client-side bounds
Each SDK batches, validates, and queues events before they reach the API. These are the shipped defaults — batch size and flush interval are configurable at init.
| Bound | Web | Node | Swift | Android | React Native |
|---|---|---|---|---|---|
| Default batch size | 20 | 20 | 20 | 20 | 20 |
| Default flush interval | 2,000 ms | 2,000 ms | 2,000 ms | 2,000 ms | 2,000 ms |
| Queue cap (events) | 1,000 | 1,000 | 1,000 | 1,000 | 1,000 |
| Queue durability | Durable (localStorage), replayed on next boot | In-memory; drained on shutdown signals, 2,000 ms bounded | Durable (disk), rehydrated on launch | Durable (disk), rehydrated on launch | Durable (persisted store), trimmed to 1,000 on restore |
| String value truncation | 1,024 chars | 1,024 chars | 1,024 chars | 1,024 chars | 1,024 chars |
| Properties byte cap (client) | 8 KB | 8 KB | — | — | 8 KB |
| Max nesting depth | 5 | 5 | 32 | 32 | 5 |
Queue overflow evicts oldest-first: when the buffer exceeds 1,000 events, the oldest events are dropped and the newest 1,000 kept. Values past the depth cap are coerced to "[depth-exceeded]"; long strings are truncated, not rejected. Swift and Android skip the client-side 8 KB serialization check — the server's 8 KB per-event cap still applies to every SDK.
The server SDK keeps its buffer in process memory — there is no durable store to replay after a crash. On SIGTERM / SIGINT / beforeExit it drains the queue once, bounded at 2,000 ms by default. For clean shutdowns, await server.flush() before exiting.
Error-capture caps
Automatic error capture is rate-limited on the client so a render loop or retry storm can't flood your project with identical reports.
| Cap | Web | Node | React Native |
|---|---|---|---|
| Reports per fingerprint per minute | 5 | 5 | 5 |
| Reports per session (all fingerprints) | 100 | 100 | 100 |
Both caps are configurable per-fingerprint sampling aside (maxPerFingerprintPerMinute, maxPerSession in the error-capture config). Excess reports are dropped client-side and never sent. Swift and Android do not enforce these client caps; the server-side per-fingerprint sampling below is the backstop for every SDK.
What happens at each limit
429 — fully throttled
When a request can't land at all, the response is HTTP 429 with two headers:
Retry-After— whole seconds to wait before retrying, computed from the bucket's deficit, minimum 1. SDKs honour it.X-Crossdeck-Sample-Rate— a 0.01–1.00 hint of what fraction of same-fingerprint events the client should locally sample for the next minute. Current SDKs ignore it; it ships for forward compatibility.
Throttled batches still keep issues visible
A throttled /v1/events batch is not all-or-nothing. 1 error event per fingerprint per minute is still ingested so new and ongoing issues stay visible on the dashboard; the rest are dropped and counted. Non-error events in a throttled batch are dropped. If anything landed, the response is 202 with a throttled block — { dropped, sampleRate, retryAfterMs } — plus the headers above. Dropped events are gone; retrying them would spend the same empty bucket, so the SDK's durable queue does not re-send them.
Reject vs soft-drop
Payload violations split two ways, per the table above. Hard caps (batch size, 8 KB properties, name / id / identity shapes) reject the whole batch with 400 and the index of the first failing event — fix that event and retry. Decoration fields (tags, categoryTags, identify traits) soft-drop: the bad entries are stripped, the event ingests, and the response reports what was dropped.
Fail-open
If the rate-limit store itself fails, the request is allowed — never throttled, never dropped. The failure is logged and alerted on Crossdeck's side. An infrastructure problem on our end never costs you events.
426 — PARK, don't drop
If an SDK's wire format is ever retired, the API answers HTTP 426 naming the minimum SDK version. Conformant SDKs PARK: events are held in the durable on-device queue and backfill automatically once the app ships an upgraded SDK — nothing is lost. See SDK event durability for the full contract.
Related
- SDK event durability — the durable queue, replay-on-boot, and the PARK-on-426 contract in depth.
- Track events — event shape, properties, and batching from the SDK side.
- Capture an error — automatic and manual error capture, fingerprinting, and sampling.
- API keys & authentication — key types and which endpoints each key may call.