# Rate limiting

> Understand how Nexpay rate limits API requests and how to handle 429 responses.

Nexpay enforces rate limits to protect the API from abuse and ensure consistent performance for all users. When you exceed a rate limit, the API responds with HTTP `429 Too Many Requests`.

---

## Default limits

The API applies a global rate limit of **200 requests per second** per client. This limit applies to all endpoints unless a stricter per-endpoint limit is in place.

### Per-endpoint limits

Some endpoints have stricter limits to prevent abuse:

| Endpoint | Limit | Window |
| --- | --- | --- |
| `POST /auth/login` | 10 requests | 60 seconds |
| `POST /auth/signup` | 5 requests | 60 seconds |
| `POST /chat` | 10 requests | 60 seconds |
| `GET /chat/:conversationId` | 30 requests | 60 seconds |

---

## Handling rate limit errors

When you exceed the rate limit, the API returns a `429` status code along with the following response headers — your retry policy should always honour `Retry-After` first:

| Header | Example | Meaning |
| --- | --- | --- |
| `Retry-After` | `5` | Seconds until the next request will be accepted. Use this verbatim before applying any client-side backoff. |
| `X-RateLimit-Limit` | `200` | Bucket ceiling for the calling key. |
| `X-RateLimit-Remaining` | `0` | Requests left in the current window. |
| `X-RateLimit-Reset` | `1717400000` | Unix epoch seconds at which the window resets. |

```javascript
{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}
```

Buckets are scoped **per API key**. Sharing one key across many pods aggregates against the same bucket — shard by key when you fan out. 503 responses for maintenance also include `Retry-After`.

### Best practices

* **Implement exponential backoff** — When you receive a `429` response, wait before retrying. Start with a short delay (e.g. 1 second) and double it on each consecutive `429` response.
* **Cache responses** — Avoid unnecessary repeated requests by caching responses locally, especially for data that doesn't change often (e.g. lookup data, payee lists).
* **Spread requests over time** — If you need to make many requests (e.g. bulk imports), spread them evenly rather than sending them all at once.
* **Use query parameters efficiently** — Retrieve multiple records per request using [pagination](/docs/queries.md) instead of fetching them one by one.

### Retry example

```javascript
const fetchWithRetry = async (url, options, maxRetries = 3) => {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status === 429 || (response.status >= 500 && response.status !== 501)) {
      // 1. Honor the server's Retry-After if present.
      const retryAfter = Number(response.headers.get('Retry-After'));
      // 2. Otherwise exponential backoff with ±30% jitter to avoid herd-fail.
      const base = Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : Math.min(30_000, 2 ** attempt * 1000);
      const jitter = base * (0.7 + Math.random() * 0.6);
      await new Promise((r) => setTimeout(r, jitter));
      continue;
    }

    return response;
  }

  throw new Error('Max retries exceeded');
};
```

The retry policy above covers `429` and transient `5xx` (502/503/504), caps the delay at 30 seconds, and adds jitter so a fleet of pods doesn't thunder-herd the next window.
