# 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).