---
title: Rate limits
description: The Qaf API allows 60 requests a minute and 1,000 searches a day per account. The rate-limit headers, the 429 response, and how to back off.
---

Limits apply per Qaf account, across all of its API keys:

| Limit | Applies to |
| --- | --- |
| 60 requests a minute | Every endpoint. |
| 1,000 searches a day | `POST /v1/search`, counted over 24 hours. |

The MCP server has its own limit, also counted per account: see [MCP server](/mcp#limits).

## Headers

Once the API has accepted your key, its response reports the account's minute window:

| Header | Value |
| --- | --- |
| `X-RateLimit-Limit` | Requests the account can make per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | Seconds until the minute's allowance resets. |

These headers always describe the minute window. A `429` for the `minute` or `day` limit has them too; on a `day` refusal, `Retry-After` is the time until searches work again instead. They're absent when the API refuses a request before it accepts the key: any `401` or `403`, an `invalid_keys` `429`, a body over 64 KiB (`413`), a body that isn't valid JSON (`400`), or a path the API doesn't have (`404`, `405`).

## When you reach a limit

The API answers `429` with a `Retry-After` header, in seconds, and the reason in the body:

```json
{
  "code": "RATE_LIMITED",
  "status": 429,
  "message": "Too many requests. Try again later.",
  "data": { "retryAfter": 23, "reason": "minute" }
}
```

| `reason` | Means |
| --- | --- |
| `minute` | The account made 60 requests this minute. |
| `day` | The account ran 1,000 searches in the last 24 hours. The other endpoints still work. `Retry-After` is the time until searches work again, which can be many hours. |
| `invalid_keys` | The key you sent doesn't exist or was revoked, and your network address has sent too many such keys. A key that works is never refused for this reason. |

## Back off and retry

Retry a `429` only when `reason` is `minute`: wait `Retry-After` seconds, then send the request again. Don't wait out the other reasons:

- **`day`:** stop searching, and tell your users when searches work again. `Retry-After` can be close to a day.
- **`invalid_keys`:** the key you send doesn't exist or was revoked. Fix it: retrying with the same key fails again.

For `500` and `503` responses, which have no `Retry-After`, wait longer after each failed attempt.

```ts TypeScript
const sleep = (seconds: number) =>
	new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function callQaf(url: string, init: RequestInit, attempts = 4) {
	for (let attempt = 1; ; attempt++) {
		const response = await fetch(url, init);

		if (response.status === 429) {
			const { data } = await response.clone().json();

			if (data?.reason === "day")
				throw new Error(`Daily search limit. Searches resume in ${data.retryAfter} s.`);

			if (data?.reason === "invalid_keys")
				throw new Error("Invalid API key. Check QAF_API_KEY.");

			if (data?.reason !== "minute" || attempt === attempts) return response;

			await sleep(Number(response.headers.get("Retry-After")));
		} else if (response.status >= 500 && attempt < attempts) {
			await sleep(2 ** attempt);
		} else {
			return response;
		}
	}
}
```

```python Python
import time

import requests


def call_qaf(method, url, attempts=4, **kwargs):
    for attempt in range(1, attempts + 1):
        response = requests.request(method, url, timeout=30, **kwargs)

        if response.status_code == 429:
            data = response.json().get("data") or {}

            if data.get("reason") == "day":
                raise RuntimeError(f"Daily search limit. Searches resume in {data['retryAfter']} s.")

            if data.get("reason") == "invalid_keys":
                raise RuntimeError("Invalid API key. Check QAF_API_KEY.")

            if data.get("reason") != "minute" or attempt == attempts:
                return response

            time.sleep(int(response.headers["Retry-After"]))
        elif response.status_code >= 500 and attempt < attempts:
            time.sleep(2**attempt)
        else:
            return response
```

Spread requests over the minute rather than sending them in bursts, and cache results you read more than once: passages rarely change.

## Need more

If your project needs higher limits, email us at contact@qaf.ai with what you're building and the volume you expect.
