# Limits & quotas > Ingest rate limits, plan quotas, the dropped response marker, and payload size caps - and how the SDK handles each. Source: https://oakdata.co/docs/concepts/rate-limits --- Ingest has two independent guards: a **per-key rate limit** that stops floods, and a **plan quota** that caps billable volume. They fail differently on purpose, and the SDK handles both automatically. ## 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 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 backs off exponentially and retries - events stay queued and send 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. ## Plan quota & the `dropped` marker Separately, every plan has a monthly **events quota** (`events_count`) with a **10% grace buffer**. Only [non-bot](https://oakdata.co/docs/concepts/bots) events count. When the quota (plus grace) is exhausted - or the account has no active plan - ingest deliberately does something 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" } ``` `dropped` is a stable marker telling the client the batch was intentionally not stored: | Name | Type | Description | | --- | --- | --- | | `quota_exceeded` | string | The plan's monthly events quota (plus the 10% grace) is used up. | | `no_active_plan` | string | The 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. A `200` counts as delivered, so the SDK **drops the batch and moves on** - correct for a condition that only resolves when the plan resets or upgrades. The `dropped` field is there so your own tooling can tell the difference. ## Payload & batch caps 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. | Name | Type | Description | | --- | --- | --- | | `Events per batch` | ≤ 500 | More returns `413` `batch_too_large`. | | `Request body (compressed)` | ≤ 2 MB | Larger returns `413` `payload_too_large`. | | `Decompressed size` | ≤ 12 MB | Upper bound after gunzip; the SDK caps an uncompressed payload at ~900 KB. | ## Client-side throttling Before anything reaches the network, the SDK throttles capture with a token bucket (a sustained rate with burst headroom), batches events, and gzips the payload. Ordinary apps never approach the server limits - these guards fire almost only on genuinely abnormal traffic. > **Read limits don't apply here** This page is about **ingest** (writing events). The read surfaces - the [REST API](https://oakdata.co/docs/api/rest) and [MCP server](https://oakdata.co/docs/api/mcp) - clamp result sizes with per-endpoint `limit` caps rather than a request-rate quota.