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

# Rate Limiting

> Token bucket rate limits, response headers, and handling strategies

The MeepaChat API rate-limits requests per IP address to prevent abuse and ensure fair resource usage.

## Limits

Two limiters are in effect:

| Limiter    | Rate                | Burst     | Applies to                                                                                                                                              |
| ---------- | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Global** | 240 req/min (4/sec) | 60 tokens | All API endpoints                                                                                                                                       |
| **Auth**   | 10 req/min          | 5 tokens  | `POST /api/auth/login`, `POST /api/auth/register`, `POST /api/auth/logout`, `GET /api/auth/callback/*`, `POST /api/auth/social`, `GET /api/bot-gateway` |

Auth endpoints have the stricter limiter applied on top of the global one to reduce brute-force risk.

## Algorithm

Both limiters use a [token bucket](https://en.wikipedia.org/wiki/Token_bucket):

* Each IP starts with a full bucket
* One token is consumed per request
* Tokens refill at the configured rate
* When the bucket is empty, the request is rejected

**Global limiter example:** a client can send 60 requests instantly (burst), then sustain 4 requests per second, or spread 240 requests evenly over a minute.

## Rate limit response

When a limit is exceeded the server returns:

**Status:** `429 Too Many Requests`

**Header:** `Retry-After: 1`

```json theme={null}
{
  "error": "rate limit exceeded"
}
```

There are no `X-RateLimit-*` headers. The `Retry-After: 1` value is fixed at 1 second regardless of how depleted the bucket is.

## IP detection

By default the limiter uses the TCP `RemoteAddr`. Forwarded headers (`X-Forwarded-For`, `X-Real-IP`) are only trusted when the request comes from a configured trusted proxy CIDR, preventing IP spoofing.

Configure trusted proxies via the `TRUSTED_PROXIES` environment variable (comma-separated CIDRs):

```
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12
```

When `RemoteAddr` is within a trusted CIDR, the leftmost IP in `X-Forwarded-For` (or `X-Real-IP`) is used as the client IP.

## Exemptions

| Endpoint                  | Behaviour                                |
| ------------------------- | ---------------------------------------- |
| `GET /api/ws` (WebSocket) | Exempt from rate limiting once connected |

## Handling 429 responses

### Exponential backoff

```typescript theme={null}
async function fetchWithBackoff(url: string, options: RequestInit, maxRetries = 5) {
  let delay = 1000;
  for (let i = 0; i < maxRetries; i++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;
    await new Promise(r => setTimeout(r, delay));
    delay *= 2; // 1s → 2s → 4s → 8s → 16s
  }
  throw new Error('Max retries exceeded');
}
```

### Client-side throttling

Spread requests proactively to stay under the limit rather than reacting to 429s:

```typescript theme={null}
class Throttler {
  private queue: Array<() => Promise<unknown>> = [];
  private running = false;
  private minDelay = 250; // 4 req/sec = 250ms between requests

  enqueue<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(() => fn().then(resolve, reject));
      if (!this.running) this.drain();
    });
  }

  private async drain() {
    this.running = true;
    while (this.queue.length > 0) {
      await this.queue.shift()!();
      await new Promise(r => setTimeout(r, this.minDelay));
    }
    this.running = false;
  }
}
```

## Prefer WebSocket over polling

For real-time updates, always use the WebSocket connection instead of polling. Polling burns through rate-limit budget and delivers slower updates.

```typescript theme={null}
// Instead of polling every second (60+ req/min per channel):
const ws = new WebSocket('wss://chat.example.com/api/ws');
ws.onmessage = (event) => {
  const { type, data } = JSON.parse(event.data);
  if (type === 'message.created') addMessage(data);
};
ws.send(JSON.stringify({ type: 'subscribe', data: { channelIds: ['456'] } }));
```

## Production tuning

For high-traffic deployments, consider implementing a Redis-backed distributed rate limiter for multi-instance setups.
