# Rate limits

> The up4 API rate limits: the three buckets, what X-RateLimit and Retry-After actually report, and how to back off after a 429.

Source: https://upfour.io/docs/api/rate-limits

The API limits requests with token buckets. A bucket holds a burst, refills at a steady rate and answers [429 rate_limit_exceeded](https://upfour.io/docs/api/errors#code-rate_limit_exceeded) when it is empty.

## The buckets

A bucket starts full. Each request takes one token and tokens come back at the rate in the table. At 1,200 per minute that is 20 requests per second, and a burst of up to 300 at once is fine.

| Bucket | Applies to | Refill | Burst |
| --- | --- | --- | --- |
| Per IP address | All requests from one address except the check-ins below, authenticated or not. Not counted: OPTIONS, /v1/health, /v1/readyz and /v1/password/forgot. | 3,000 per minute | 300 |
| Per API key | Every authenticated request, counted per key. | 1,200 per minute | 300 |
| Per heartbeat slug and IP | Check-ins to /v1/heartbeat/{slug}. These calls need no key. | 120 per minute | 20 |

An API call takes a token from the IP bucket first and then from the key bucket, and fails if either is empty. Idempotent replays count like any other request. Login, registration, contact and the public status page forms have their own, tighter buckets per IP address.

## Headers

| Header | Value |
| --- | --- |
| `X-RateLimit-Limit` | The refill rate of the IP bucket in requests per minute. |
| `X-RateLimit-Remaining` | Whole requests left in the IP bucket after this one. |
| `X-RateLimit-Reset` | A Unix timestamp in seconds, the moment the IP bucket has refilled one request. The bucket refills continuously, so this is about now, not the end of a window. |
| `Retry-After` | Only on 429: whole seconds to wait, at least 1. |

The `X-RateLimit-*` headers describe the IP bucket, not the key bucket. A key that has used up its own 300-request burst gets `429` and `Retry-After: 1` while the headers still show a healthy IP bucket. Treat `429` and `Retry-After` as the signal, not `Remaining`.

```bash
curl -si https://app.upfour.io/v1/monitors \
  -H "Authorization: Bearer $UP4_API_KEY" | grep -i -E '^(HTTP|x-ratelimit|retry-after)'
```

```
HTTP/2 429
retry-after: 1
x-ratelimit-limit: 3000
x-ratelimit-remaining: 299
x-ratelimit-reset: 1793102400

{ "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "request_id": "req_…" } }
```

## After a 429

1. Stop sending for the number of seconds in `Retry-After`.
2. Retry the same request. A `POST` that carries an `Idempotency-Key` is safe to repeat.
3. If the next answer is again 429, wait longer each time and add a random delay, so parallel workers do not retry together.

Spread bulk work instead of racing it: `POST /monitors/batch` and `POST /monitors/bulk` create or change many monitors in one request.

## How the counts are kept

When up4 runs its shared counter store, all API servers count together. If that store is not available, each server counts for itself, so the ceiling you can reach during such a period can be higher, never unlimited.

## Common mistakes

- Waiting until `X-RateLimit-Reset`. It is almost now; use `Retry-After`.
- Reading `Remaining` as your key’s budget. It is the IP bucket’s.
- Running many workers behind one address and one key. They share both buckets.
- Retrying at once in a tight loop. Every retry takes a token too.
- Polling for state changes. A [webhook](https://upfour.io/docs/api/webhooks.md) sends them to you.
