# Rate Limits

Per-key request limits, rate limit headers, and the size limits that apply to requests and posts.


Relaycast limits how fast each access key can call the API, and how large requests and posts can be. Platforms also enforce their own limits, which are covered in the [platform guides](/docs/platforms).

## Request rate limit

Each access key can make **120 requests per minute** by default. The limit applies per key, so separate keys (for example one per service) each get their own allowance.

The window is fixed to the clock minute: the count resets at the start of each minute rather than sliding.

### Headers

Authenticated responses include:

| Header | Description |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per minute for this key. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |

```http
HTTP/1.1 201 Created
Content-Type: application/json
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-Request-Id: 2f6c0a3e-...
```

### When you hit the limit

Requests over the limit get `429`:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded, retry in a minute"
  }
}
```

There's no `Retry-After` header. Wait until the next minute starts, then retry with exponential backoff. For `POST /v1/posts`, retry with the same `Idempotency-Key` so a request that did get through isn't duplicated. See [Idempotency & Retries](/docs/idempotency).

```ts title="retry.ts"
async function withRateLimitRetry(doRequest: () => Promise<Response>, maxAttempts = 4) {
  for (let attempt = 1; ; attempt++) {
    const res = await doRequest();
    if (res.status !== 429 || attempt >= maxAttempts) return res;
    // Wait for the next minute window, plus a little jitter.
    const msToNextMinute = 60_000 - (Date.now() % 60_000);
    await new Promise((r) => setTimeout(r, msToNextMinute + Math.random() * 1_000));
  }
}
```

Rate-limited requests appear in [Request Logs](/docs/logs) with status `429`.

> [!NOTE]
> If the rate limiter itself is briefly unavailable, requests are allowed through rather than blocked, and the rate limit headers are left out of those responses. Don't treat missing headers as an error.

### Staying under the limit

- Fan out in one request. A single post can target up to 20 accounts, which counts as one request.
- Use [webhook notifications](/docs/notifications) instead of polling `GET /v1/posts/{id}`.
- Cache `GET /v1/accounts`. Accounts change rarely.
- Use one key per service so a busy job can't starve another.

## Size limits

| Limit | Value |
| --- | --- |
| Request body | 1 MB |
| `content` and `targets[].content` | 10,000 characters (platform limits are usually lower) |
| Targets per post | 1–20 |
| Media items per post | 10 |
| Media `url` length | 2,048 characters |
| Media `altText` | 1,000 characters |
| Media `title` (documents) | 400 characters |
| `GET /v1/posts` page size | `limit` 1–100, default 20 |
| Media library upload | 150 MB per file (dashboard) |

Requests over 1 MB are rejected with `413` (`payload_too_large`) before they're processed. Media is passed by URL, so the body limit only covers the JSON itself.

## Platform limits

Each platform has its own rules on text length, media count, file size, video duration and how often an account may post. Relaycast validates the static rules (such as character counts and media counts) when you create the post, so you get a `400` right away. Limits that only the platform knows, such as daily posting caps, surface at publish time as a failed target with `errorCode: "rate_limited"`, which is retried automatically with backoff. See the [platform guides](/docs/platforms).

## Credits

Rate limits and credits are separate. A request within the rate limit can still fail with `402` if your project is out of credits. See [Credits & Usage](/docs/credits).
