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

# Rate Limiting

> How the folksbase API limits request rates to prevent abuse, including the global and upload-specific limiters.

## Overview

The folksbase API uses [Upstash Ratelimit](https://github.com/upstash/ratelimit) backed by Redis to enforce request limits. There are two limiters, each with a different scope and threshold. Both use a sliding window algorithm, which distributes requests more evenly than fixed windows.

## Limiters

### Global Limiter

Applied to all `/api/*` routes. Identifies clients by IP address.

| Setting      | Value                                        |
| ------------ | -------------------------------------------- |
| Window       | 60 seconds (sliding)                         |
| Max requests | 100 per window                               |
| Identifier   | Client IP (`X-Forwarded-For` or `X-Real-IP`) |
| Redis prefix | `folksbase:rl`                               |

### Upload Limiter

Applied only to `POST /api/imports` (CSV file uploads). This is stricter because uploads are expensive — each one triggers blob storage, CSV parsing, AI column mapping, and background processing.

| Setting      | Value                                     |
| ------------ | ----------------------------------------- |
| Window       | 10 minutes (sliding)                      |
| Max requests | 5 per window                              |
| Identifier   | Authenticated user ID, falling back to IP |
| Redis prefix | `folksbase:rl:upload`                     |

Upload requests hit both limiters. A request must pass the global limiter first, then the upload limiter.

## Response Headers

Every response from a rate-limited endpoint includes these headers:

| Header                  | Description                                    |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window       |
| `X-RateLimit-Reset`     | Unix timestamp (ms) when the window resets     |

When a request is rejected, an additional header is included:

| Header        | Description                        |
| ------------- | ---------------------------------- |
| `Retry-After` | Seconds until the client can retry |

## Rate Limit Exceeded

When either limiter rejects a request, the API returns a `429` response:

```json theme={"dark"}
{
  "code": "RATE_LIMITED",
  "message": "Too many requests"
}
```

For the upload limiter specifically:

```json theme={"dark"}
{
  "code": "RATE_LIMITED",
  "message": "Too many uploads. Try again later."
}
```

## Why Two Limiters?

The global limiter protects the API from general abuse — bots, scrapers, or runaway scripts. 100 requests per minute is generous for normal usage.

The upload limiter exists because CSV imports are resource-intensive. A single upload can trigger:

* Blob storage write (up to 200 MB)
* CSV parsing and validation
* AI-powered column mapping via Anthropic
* Background job processing with chunked database inserts
* Email notification on completion

Without a separate limit, a client could exhaust server resources by uploading repeatedly within the global 100-request window.

## Client Best Practices

* Check `X-RateLimit-Remaining` before making requests in tight loops
* When you receive a `429`, wait for the duration specified in `Retry-After` before retrying
* For bulk operations, use the CSV import feature instead of individual `POST /api/contacts` calls
* The frontend already handles retries for transient failures (502, 503, 504) with a 1-second delay
