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

# Rate Limits

> How rate limiting works, which headers to read, and how to handle 429 responses with exponential backoff.

The Causeloop API enforces a **sliding-window rate limit per client IP**. All requests — authenticated or not — count against the limit for the originating IP address.

## Limits

| Window     | Max requests |
| ---------- | ------------ |
| 60 seconds | 600          |

<Note>
  The limit is applied per IP address (or the first value of `X-Forwarded-For` when the request passes through a proxy). Requests from different IPs are counted independently.
</Note>

When Redis is configured (`REDIS_URL`), the limit is enforced across all API replicas using a shared fixed-window counter. Without Redis, the counter is per-process — a single IP hitting multiple replicas gets a higher effective limit. In production, configure Redis for consistent enforcement.

## Rate limit response headers

Every response — including successful ones — carries two rate limit headers:

| Header                  | Description                                       |
| ----------------------- | ------------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed in the 60-second window. |
| `X-RateLimit-Remaining` | Requests remaining in the current window.         |

When the limit is exceeded, the `429` response also includes:

| Header                  | Description                                   |
| ----------------------- | --------------------------------------------- |
| `Retry-After`           | Seconds to wait before the next window opens. |
| `X-RateLimit-Remaining` | Always `0` on a `429`.                        |

## 429 response body

```json theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests"
  }
}
```

<Note>
  The `429` error body does not include a `trace_id`. Use the `Retry-After` header value, not a fixed delay, to determine how long to wait.
</Note>

## Handling rate limits

Read the `X-RateLimit-Remaining` header on every response. When it reaches zero or you receive a `429`, back off and retry:

<CodeGroup>
  ```bash curl theme={null}
  # Read headers from any response
  curl -I https://api.causeloop.ai/v1/issues \
    -H "Authorization: Bearer $CAUSELOOP_TOKEN"

  # HTTP/2 200
  # x-ratelimit-limit: 600
  # x-ratelimit-remaining: 598
  ```

  ```javascript JavaScript theme={null}
  async function callWithRetry(url, options, maxRetries = 4) {
    let delay = 1000;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      const resp = await fetch(url, options);

      if (resp.status !== 429) return resp;

      const retryAfter = parseInt(resp.headers.get('Retry-After') ?? '1', 10);
      const jitter = Math.random() * 500;
      const wait = Math.max(retryAfter * 1000, delay) + jitter;

      console.warn(`Rate limited. Retrying in ${Math.round(wait)}ms (attempt ${attempt + 1})`);
      await new Promise(r => setTimeout(r, wait));
      delay *= 2; // exponential backoff
    }

    throw new Error('Rate limit retries exhausted');
  }

  // Usage
  const resp = await callWithRetry(
    'https://api.causeloop.ai/v1/issues',
    { headers: { 'Authorization': `Bearer ${accessToken}` } },
  );
  ```

  ```python Python theme={null}
  import time
  import random
  import requests

  def call_with_retry(url: str, max_retries: int = 4, **kwargs) -> requests.Response:
      delay = 1.0

      for attempt in range(max_retries + 1):
          resp = requests.get(url, **kwargs)

          if resp.status_code != 429:
              return resp

          retry_after = int(resp.headers.get('Retry-After', 1))
          jitter = random.random() * 0.5
          wait = max(retry_after, delay) + jitter

          print(f'Rate limited. Retrying in {wait:.1f}s (attempt {attempt + 1})')
          time.sleep(wait)
          delay *= 2  # exponential backoff

      raise RuntimeError('Rate limit retries exhausted')

  # Usage
  resp = call_with_retry(
      'https://api.causeloop.ai/v1/issues',
      headers={'Authorization': f'Bearer {access_token}'},
  )
  ```
</CodeGroup>

## Best practices

**Read `X-RateLimit-Remaining` proactively.** If it's low, slow down your request rate before hitting the limit.

**Implement exponential backoff with jitter.** Start at the `Retry-After` value, then double the delay on each subsequent `429`. Add random jitter (±500 ms) to avoid thundering-herd when multiple clients retry in sync.

**Avoid polling for status.** Use the [WebSocket stream](/api-reference/realtime-websocket) for real-time updates — WebSocket messages do not count against REST rate limits. For endpoints that do require polling:

| Endpoint                                | Safe poll interval           |
| --------------------------------------- | ---------------------------- |
| `GET /v1/reports/jobs/{id}`             | Every 5 seconds              |
| `GET /v1/exports/{id}`                  | Every 10 seconds             |
| `GET /v1/me/notifications/unread-count` | Every 30 seconds             |
| `GET /v1/shell/nav`                     | On app focus, not on a timer |

**Batch requests where possible.** Filter lists server-side (`?status=open`) rather than fetching all records and filtering client-side.
