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

Limits

Two limiters are in effect:
LimiterRateBurstApplies to
Global240 req/min (4/sec)60 tokensAll API endpoints
Auth10 req/min5 tokensPOST /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:
  • 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
{
  "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

EndpointBehaviour
GET /api/ws (WebSocket)Exempt from rate limiting once connected

Handling 429 responses

Exponential backoff

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:
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.
// 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.