# Rate limits & errors

Plan limits, 429 handling, and the error status values.

## Plan limits

Limits attach to your **organization**, not to individual API keys — every key in a workspace draws from the same pool, so creating more keys never raises a limit. There are three axes: concurrent streams, requests per minute, and characters per calendar month. Your workspace’s exact limits are shown in the [console settings](https://platform.kenpathlabs.com/dashboard/settings).

| Limit | Free workspace |
|---|---|
| Concurrent WebSocket streams | 1 |
| Concurrent HTTP streams | 2 |
| Requests / minute | 10 |
| Characters / month | 10,000 |

> For higher limits, email [hello@kenpathlabs.com](mailto:hello@kenpathlabs.com) with your expected concurrency and monthly character volume. WebSocket and HTTP allowances are set independently, so a workload that is mostly one channel can be sized that way.

- **Concurrent streams**: in-flight requests at once, across every key in the workspace. Each open connection (including a WebSocket) holds one slot; slots free on completion, and a crashed connection’s slot self-frees on a short TTL.
- **Requests per minute**: fixed 60-second window, counted per organization.
- **Characters per month**: summed `input` length over the calendar month (UTC), per organization.
- **WebSocket vs HTTP**: the two channels are counted separately, and a plan can cap each on its own — on the free tier one long-lived WebSocket and two HTTP streams can be in flight at once. The console shows your workspace’s exact per-channel numbers.

## Handling 429s

When a limit is hit you get `429` with a machine-readable `status` and, where applicable, a `Retry-After` header (seconds). Successful responses also carry your remaining budget:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `x-ratelimit-remaining-requests` | header | - | Requests left in the current minute window. |
| `x-ratelimit-remaining-streams` | header | - | Concurrency slots free for your workspace right now. |
| `x-ratelimit-remaining-characters` | header | - | Characters left this month (absent on unlimited plans). |

Back off and retry on `rate_limit_exceeded` and `too_many_concurrent_requests` (retry after ~1 s for concurrency). Treat `insufficient_quota` as terminal until the month rolls over or your plan changes; don’t retry-storm it.

## Error catalogue

All errors share one shape, so a single handler covers them: `{ "detail": { "status": "...", "message": "..." } }`.

```
{
  "detail": {
    "status": "rate_limit_exceeded",
    "message": "Request rate limit exceeded."
  }
}
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `rate_limit_exceeded` | 429 | - | Too many requests this minute. Respect Retry-After. |
| `too_many_concurrent_requests` | 429 | - | All concurrency slots for your workspace are in use. Retry in ~1 s. |
| `insufficient_quota` | 429 | - | Monthly character quota exhausted. Terminal until reset/upgrade. |
| `missing_api_key / invalid_api_key` | 401 | - | Auth problem; see [Authentication](https://docs.kenpathlabs.com/authentication.md). |

> These `status` values intentionally match the OpenAI and ElevenLabs error vocabularies, so those SDKs’ built-in retry logic works against Svara unchanged.
