> ## Documentation Index
> Fetch the complete documentation index at: https://teardowns.aero/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limiting

> The four buckets that govern API-key traffic, and how to design your ERP around them.

The public API rate-limits **only** key-authenticated traffic. The web
UI (JWT) is never throttled by our limiter. So your integration's
traffic budget is independent of what your users do in the web app.

## The four buckets

| Bucket | Scope                                            | Limit                                      | What it protects                                    |
| ------ | ------------------------------------------------ | ------------------------------------------ | --------------------------------------------------- |
| **A**  | per API key, JSON endpoints                      | **600 req/min** (10/s sustained, burst 60) | Runaway loops, retry storms.                        |
| **B**  | per source IP, **failed** auth on `/public/v1/*` | **30 fails/min**                           | Brute-force keyspace scanning.                      |
| **C**  | per source IP, `/org/api-keys` mint endpoint     | **10 req/min**                             | Abuse of the issuance UI.                           |
| **D**  | per API key, upload endpoints                    | **60 req/min** (burst 5)                   | Supabase Storage egress. 60 × 50 MB = 3 GB/min cap. |

Each bucket is independent a request only consumes from the one(s) it
falls into. A regular JSON request consumes from A; a multipart upload
consumes from D; a failed auth consumes from B (regardless of which
endpoint).

## What happens on overflow

Response:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{
  "detail": {
    "error_code": "too_many_requests",
    "message": "Rate limit exceeded for this API key.",
    "limit": "600 per 1 minute"
  }
}
```

The `Retry-After` header is in seconds. Wait at least that long before
retrying. Treat the value as authoritative our backend recalculates
it from the actual window state.

## Designing your ERP around the limits

A few patterns that work in practice:

<AccordionGroup>
  <Accordion title="Batch jobs that need a higher budget">
    Mint a second key just for the batch process. Each key has its own
    600 rpm bucket. A nightly bulk-import script with its own key
    doesn't impair the day-to-day key.
  </Accordion>

  <Accordion title="Retries with backoff">
    On any 5xx or 429, retry with exponential backoff. Capped at, say,
    5 retries with a base of 1 second and a max of 60.
    Always respect `Retry-After` when present it's better than your
    backoff math.

    ```python theme={null}
    import time, requests
    def post_with_retry(url, **kw):
        for attempt in range(5):
            r = requests.post(url, **kw)
            if r.status_code == 429:
                wait = int(r.headers.get('Retry-After', 2 ** attempt))
                time.sleep(wait)
                continue
            if r.status_code >= 500:
                time.sleep(2 ** attempt)
                continue
            return r
        r.raise_for_status()
    ```
  </Accordion>

  <Accordion title="Bursting safely">
    Bucket A allows bursts up to 60 req. If you've been idle, you can
    fire 60 in a second. After that the leaky bucket refills at 10/s.
    So a `for` loop firing 100 requests will burst the first 60, then
    receive 429s for the next 40 until enough time has passed.

    Cleaner: just paced the loop at one request per 100 ms. Predictable
    and never trips the limiter.
  </Accordion>

  <Accordion title="Parallel uploads">
    Bucket D is 60/min with a tighter burst of 5. Parallel uploads in
    excess of 5 will start receiving 429s. Cap your upload concurrency
    at \~5 and queue the rest. The math:

    * 5 simultaneous uploads, each 30 seconds → cycle every \~30 s.
    * In one minute you can clear \~10 uploads at 5x concurrency.
    * 60 rpm bucket gives you headroom for 60 uploads in a minute,
      provided you don't spike the burst.
  </Accordion>

  <Accordion title="Polling vs. webhooks">
    There are no public-API webhooks today, so partners sometimes poll.
    For a status-check loop:

    * Poll every 5 minutes for slow-moving state (e.g., counter-offers).
    * Poll every 30 seconds for state that should change quickly.
    * Never poll faster than 1/sec.

    All of these stay well within the 10/s sustained rate of bucket A.
  </Accordion>
</AccordionGroup>

## Visibility

* Every 429 includes a `Retry-After` header. That's your primary
  signal.
* Every failed auth (401s on `/public/v1/*`) consumes bucket B. If your
  ERP is hammering with a wrong key, you'll start getting 429s on top
  of the 401s. Fix the key.
* Mint and rotation calls hit bucket C. A misbehaving CI job that
  re-mints keys in a loop will trip this within a minute.

## What's NOT rate-limited

* **Anything authenticated with a JWT.** Web app, internal scripts that
  use a Supabase JWT, etc. Out of scope for this limiter.
* **Successful requests by an org's other keys.** Bucket A is per
  individual key. If your org has three keys, each gets its own 600
  rpm.

## When the platform behavior changes

The numbers above are the production defaults. We may raise the limits
for specific partners on request email `support@teardowns.aero` with
the integration name and a sense of the traffic you expect. We're
unlikely to **lower** the limits without notice; if we ever did, you'd
hear about it via the changelog and via direct email if you're an
active integration.

## Implementation note

We use `slowapi` (a FastAPI rate-limit middleware) with a custom
key-func that reads the bearer token directly from the `Authorization`
header. UI traffic without a `tdao_live_` prefix is exempt. Counts are
held in-memory per process with N worker processes the effective
limit per key per minute is N × the configured value (worst case). For
the 600 rpm default that's not a meaningful difference in practice.
