# Rate Limits

> Per-API-key, per-minute rate limits by tier (Sandbox/Starter 100, Growth 500, Enterprise 2,000). Both environments expose X-RateLimit headers; a sliding 1-minute window returns 429 with Retry-After.

Limits are applied per API key, per minute. Every response includes rate-limit headers so you can
track consumption and back off gracefully.

| Tier | Requests / min | Concurrent streams | Notes |
| --- | --- | --- | --- |
| Sandbox / Starter | 100 | 2 | Default for all new sandbox and starter accounts. |
| Growth | 500 | 5 | Available on the Growth plan. Contact us to upgrade. |
| Enterprise | 2,000 | 20 | Custom limits above 2,000 req/min — contact partnerships@mystocks.africa. |

The limit is keyed to your **partner identity**, not to a single credential: a full key and all of its
data keys share one bucket, and the limit survives a key rotation.

## Concurrent stream limit

`GET /stream` (Server-Sent Events) is capped at the **concurrent streams** shown above. Opening
more returns `429 RATE_LIMITED` with `activeConnections` and `limit` in the error body, rather than
opening a stream that immediately dies.

A stream holds its slot for as long as it is receiving heartbeats. If a connection drops without a
clean close, its slot frees itself automatically within ~45 seconds — so a reconnect loop cannot
permanently exhaust your own limit. One stream carries every event type; you do not need a connection
per symbol or per sub-account.

## Rate-limit headers

Sent on every authenticated sandbox and production response.

```http
X-RateLimit-Limit: 100         # your tier's request ceiling per minute
X-RateLimit-Remaining: 43      # requests remaining in the current window
X-RateLimit-Reset: 1751400060  # Unix timestamp (seconds) when the window resets
Retry-After: 17                # seconds until reset (only set on 429 responses)
```

The window is a **1-minute sliding window**. Exceeding the limit returns `429` immediately — requests
are _not_ queued or delayed. Use `Retry-After` (seconds) or `X-RateLimit-Reset` (epoch) to know when
to retry.

```jsonc
// 429 response body
{ "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Tier: starter (100 req/min). Resets in 17s." } }
```

## Backoff example (Node.js)

```javascript
async function fetchWithBackoff(url, options, retries = 3) {
  const res = await fetch(url, options);
  if (res.status === 429 && retries > 0) {
    const reset = Number(res.headers.get('X-RateLimit-Reset'));
    const delay = Math.max(reset * 1000 - Date.now(), 1000);
    await new Promise((r) => setTimeout(r, delay));
    return fetchWithBackoff(url, options, retries - 1);
  }
  return res;
}
```

<Callout type="info">
- Read endpoints (`GET /stocks`, `GET /quote/{symbol}`) are cheaper to cache locally — avoid polling per user request.
  - Batch sub-account operations: list all orders once per minute rather than one request per user.
  - Webhook callbacks do **not** count toward your rate limit.
  - Rate limits apply to sandbox and production keys independently.
</Callout>
