# API limits

<!-- Generated by `pnpm --filter @bucker/api limits:emit` from the limiter configuration.
     Do not edit by hand: a test fails when this page and the code disagree. -->

Every limit here is a token bucket that refills continuously over one minute. Limits are held
in memory per API machine, so a deployment with several machines allows up to that many times
the figure in total. Treat them as ceilings against abuse and accident, not as quotas.

A limited request is answered **429** with a `Retry-After` header in whole seconds. Wait that
long; do not retry sooner.

## The control plane (the REST API)

| Limit | Applies to | Per minute | Keyed on |
| --- | --- | --- | --- |
| Organization | Every authenticated `/orgs/:orgSlug/…` request, across all credentials in the organization | 3,000 | the organization |
| Credential | Every authenticated request | 900 | the user, agent or service principal |
| Address | Every route that has no tighter limit below | 1,000 | the client address (an IPv6 address counts as its /64) |
| Credential guessing | `/agents/token`, `/auth/email/verify/request`, `/auth/login`, `/auth/mfa/disable`, `/auth/mfa/enable`, `/auth/mfa/recovery-codes`, `/auth/mfa/setup`, `/auth/mfa/verify`, `/auth/mfa/webauthn/credentials/:id/remove`, `/auth/mfa/webauthn/options`, `/auth/mfa/webauthn/register/options`, `/auth/mfa/webauthn/step-up`, `/auth/mfa/webauthn/step-up/options`, `/auth/password/change`, `/auth/password/reset/confirm`, `/auth/password/reset/request`, `/auth/register`, `/auth/social/:provider/link`, `/auth/social/:provider/unlink`, `/auth/social/handoff`, `/oauth/device/approve`, `/oauth/device/lookup`, `/oauth/token-exchange`, `/sso/handoff` | 10 | the client address |
| OAuth server | `/oauth/authorize`, `/oauth/device_authorization`, `/oauth/revoke`, `/oauth/token` | 60 | the client address |
| SSO callbacks | `/sso/oidc/:orgSlug/callback`, `/sso/saml/:orgSlug/acs` | 300 | the organization |

The organization and credential limits are both checked; the tighter one refuses first.

### Response headers

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | The ceiling of the bucket closest to empty |
| `X-RateLimit-Remaining` | Requests left in that bucket |
| `X-RateLimit-Reset` | Seconds until that bucket has refilled |
| `X-RateLimit-Scope` | Which bucket the numbers describe: `organization` or `principal` (a credential). Absent when the address bucket is the one speaking |
| `Retry-After` | On a 429 only: seconds to wait |

## Sending events (ingest)

These apply to the DSN-authenticated doors: the envelope endpoint, the Sentry-compatible
endpoints, OTLP and the platform log drains.

| Limit | Per minute | Keyed on |
| --- | --- | --- |
| DSN key (default) | 500 | the key; each key can carry its own value up to 1,000,000 |
| Address, before the key is looked up | 6,000 | the client address |

A limited ingest request is answered **429** with `Retry-After` and an `X-Sentry-Rate-Limits`
header, which every Sentry-compatible SDK reads to pause the categories it names. The
`X-RateLimit-*` headers above are sent on ingest too. A key that is over its limit counts
those events as dropped (reason `key_rate_limit`) in the drop dashboard.

An accepted request carries `X-Bucker-Ingest-Disposition`, naming what was done with it (for
example `filtered` when an inbound filter dropped it), so a 200 that stored nothing is
explainable.

## Browser feedback and public status pages

| Limit | Per minute | Keyed on |
| --- | --- | --- |
| User feedback, per submitter | 20 | the submitter's address (/64 for IPv6) |
| User feedback, per DSN key | 300 | the key |
| Public status page reads | 600 | the page |
| Public status, any page | 1,200 | the client address |
| Status page subscribe | 5 | the client address |
| Status confirm and unsubscribe links | 5 | the subscriber |

## Working inside the limits

- Read `X-RateLimit-Remaining` and slow down before it reaches zero.
- Send an `Idempotency-Key` header on creating POSTs so a retry after a 429 or a dropped
  connection cannot create a second copy.
- Batch where the API lets you: ingest takes envelopes of many events, OTLP takes many records
  per request.
- Use a separate credential for each automation. The credential limit is per principal, so one
  runaway script does not spend a colleague's share of the organization's.
