# Rate limits

Hangar enforces per-user sliding-window limits in Postgres. The window
is one minute (some surfaces are tighter). The same row that enforces
the limit also returns the headers — there is no separate limiter
service.

## Headers

Every limit-aware response includes:

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | Requests allowed in the current window. |
| `X-RateLimit-Remaining` | Requests left in the current window. |
| `X-RateLimit-Reset` | Unix epoch seconds at which the window resets. |
| `Retry-After` | (On 429) seconds to wait before the next attempt. |

A 429 response also carries a JSON body:

```json
{
  "error": "rate-limited",
  "message": "Too many requests. Retry after 12s.",
  "retryAfterSeconds": 12
}
```

## Defaults

| Surface | Limit |
|---|---|
| `/api/mcp` | 60 / 60s per token |
| `/api/llm/proxy` | 240 / 60s per machine |
| `/api/instance/*` | 30 / 60s per user |
| `/api/wallet/topup` | 5 / 60s per user |
| `/api/tokens` (POST) | 10 / 60s per user |
| `/api/checkout` | 10 / 60s per user |
| `/api/skills` | 60 / 60s per user |

These are subject to change as the platform scales. Always read
`X-RateLimit-Limit` rather than hardcoding the number.

## Backoff strategy

For 429s, honor `Retry-After` strictly. If the header is missing
(unusual), use exponential backoff capped at 32 seconds:

```ts
async function withBackoff<T>(fn: () => Promise<Response>): Promise<T> {
  let delay = 1_000
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fn()
    if (res.ok) return res.json()
    if (res.status !== 429) throw new Error(`HTTP ${res.status}`)
    const retryAfter = Number(res.headers.get('retry-after')) * 1_000
    await sleep(retryAfter || delay)
    delay = Math.min(delay * 2, 32_000)
  }
  throw new Error('exceeded retries')
}
```

For 5xx errors, the same exponential pattern applies; the retry is
safe because every mutating Hangar endpoint is idempotent on the same
`Idempotency-Key` header (when supplied).
Rate limits — Hangar