Rate limits & quotas

Ingest rate limits, plan quotas, the dropped response marker, and payload size caps — and how the SDK handles each.

OakData protects ingest with two independent guards: a per-key rate limit that stops floods, and a plan quotathat caps billable volume. They fail differently on purpose — and the SDK is built to handle both — but it's worth understanding how each behaves.

Per-key rate limit

Each API key may send at most 1,200 requests per minute to the ingest endpoint (a fixed window). A healthy browser client flushes at most ~12 requests/minute, so even a large site with hundreds of concurrent visitors stays far under this — the limit exists to contain a leaked or abused key. Exceed it and ingest responds:

429 Too Many Requests
http
HTTP/1.1 429 Too Many Requests
Retry-After: 30

{ "error": "rate_limited" }

The SDK honors this automatically, backing off exponentially before retrying — events stay queued and are sent once the window clears, so nothing is lost to a brief spike.

Fails open

The rate counter is best-effort: if the check itself errors, ingest accepts the request rather than dropping data. The limit throttles abuse without putting your analytics at the mercy of the limiter.

Plan quota & the dropped marker

Separately, every plan has a monthly events quota (events_count) with a built-in 10% grace buffer. Only non-bot events count toward it. When an account exhausts the quota (past the grace) — or has no active plan at all — ingest does something deliberately different from the rate limit: it returns HTTP 200 with a dropped marker instead of an error.

200 OK — quota exhausted
json
{
  "ok": true,
  "inserted": 0,
  "snapshots": 0,
  "dropped": "quota_exceeded"
}

The dropped field is a stable marker telling the client the batch was intentionally not stored. quota_exceeded means the plan limit (plus grace) is used up; no_active_plan means there is no plan to bill against.

dropped valueTypeDescription
quota_exceededstringThe plan's monthly events quota (plus the 10% grace) is exhausted.
no_active_planstringThe account has no active plan, so events can't be billed or stored.

Why 200, not 429?

A 429 tells a client to retry. But an over-quota batch will never be accepted, so retrying would loop forever and drain the device. Returning 200 with dropped signals the SDK to clear that batch from its queueand move on — the correct behaviour for a condition that won't resolve until the plan resets or upgrades.

Payload & batch caps

A few hard ceilings bound any single ingest request. The SDK stays well under them on its own; they exist to stop a runaway client or a decompression bomb.

LimitTypeDescription
Events per batch≤ 500More returns 413 batch_too_large.
Request body (compressed)≤ 2 MBLarger returns 413 payload_too_large.
Decompressed size≤ 12 MBUpper bound after gunzip; the SDK caps an uncompressed payload at ~900 KB.

Client-side throttling

Before anything reaches the network, the SDK throttles capture client-side with a token bucket (a sustained rate with burst headroom), batches events, and gzips the payload. The result is that ordinary apps never approach the server limits — these guards almost only ever fire on genuinely abnormal traffic.

Read limits don't apply here

Everything on this page is about ingest (writing events). The read surfaces — the REST API and MCP server — are authenticated per key and scoped per project; they clamp result sizes with per-endpoint limit caps rather than a request-rate quota.