# Rate limits

> SerpKite limits requests per second per API key. Free keys get 5 per second; buying a pack raises every key to that pack's rate. Over the limit you get a 429 with Retry-After.

## Limits per plan

Rate limits are **requests per second, per API key**. They protect the upstream and keep latency predictable; they are not a quota. Your total volume is bounded only by your credit balance and any [spend controls](https://serpkite.com/docs/spend-controls) you set.

| Plan | Requests per second, per key |
| --- | --- |
| Free | 5 |
| Starter | 20 |
| Growth | 50 |
| Pro | 100 |
| Enterprise | Custom |

Your rate is set by the **largest pack you have ever bought**, not by your current balance. Buy a Pro pack once and every key on the account stays at 100 requests per second, even after the credits are spent and you top up with Starter packs. [`GET /v1/account`](https://serpkite.com/docs/endpoints/account) returns the current value as `rate_limit_rps`.

Because the limit is per key, two services with separate keys don't compete for the same budget. Keys on a [team](https://serpkite.com/docs/team) account all get the owner's rate.

## What counts as a request

- Every call to a search endpoint counts as one request.
- A [`POST /v1/batches`](https://serpkite.com/docs/batch) call counts **once**, however many requests it holds, so every plan can submit the full 100 per call. The jobs then run on the batch lane at their own pace, so batch is the way to push large volumes without managing concurrency yourself. What bounds a batch is the queue: a single account can have up to 10,000 jobs queued at once.
- `GET /v1/account` and `GET /v1/batches/{id}` are free but still authenticated calls. Poll batch jobs every few seconds, not in a tight loop, or use a [webhook](https://serpkite.com/docs/webhooks) instead.
- [MCP](https://serpkite.com/docs/mcp) tool calls count like the equivalent REST call.

## When you hit the limit

Over the limit, the API answers `429 Too Many Requests` with the `rate_limited` code and a `Retry-After` header (seconds). Nothing is billed.

```http
HTTP/2 429
content-type: application/json
retry-after: 1
x-request-id: req_01J8ZKA1B2C3D

{"error":{"code":"rate_limited","message":"rate limit exceeded","request_id":"req_01J8ZKA1B2C3D"}}
```

`Retry-After` is exposed through CORS, so browser code can read it too.

Two other 429 codes are **not** about request rate, and retrying soon won't help:

- `daily_limit_reached`: a free account hit its daily cap. Buying any pack removes it.
- `rate_limited` with a message about queued batch jobs: you have too many batch jobs waiting. Let some finish first.

## Retrying well

The official SDKs already do this: they honour `Retry-After` and retry `429` and `5xx` with exponential backoff (configurable with `maxRetries`, `max_retries` or `serpkite.WithMaxRetries`). With your own HTTP client, wait for `Retry-After`, then retry with exponential backoff and jitter. The same approach works for `503` (upstream errors, blocks and timeouts), which is also free and safe to retry.

Python:

```python
import os
import random
import time

import requests

API = "https://api.serpkite.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"}
RETRY = {429, 500, 502, 503, 504}

def search(body: dict, attempts: int = 5) -> dict:
    for i in range(attempts):
        res = requests.post(f"{API}/v1/search", json=body, headers=HEADERS, timeout=30)
        if res.status_code not in RETRY:
            res.raise_for_status()
            return res.json()
        code = res.json()["error"]["code"]
        if code == "daily_limit_reached":
            raise RuntimeError("free-tier daily limit reached")
        wait = float(res.headers.get("Retry-After", 0)) or min(2**i, 30)
        time.sleep(wait + random.random() / 2)
    res.raise_for_status()
    return res.json()
```

Node.js:

```javascript
const RETRY = new Set([429, 500, 502, 503, 504]);

export async function search(body, attempts = 5) {
  for (let i = 0; ; i++) {
    const res = await fetch("https://api.serpkite.com/v1/search", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SERPKITE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    const { error } = await res.json();
    if (!RETRY.has(res.status) || i + 1 >= attempts || error.code === "daily_limit_reached") {
      throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
    }
    const wait = Number(res.headers.get("Retry-After")) || Math.min(2 ** i, 30);
    await new Promise((r) => setTimeout(r, wait * 1000 + Math.random() * 500));
  }
}
```

Go:

```go
func search(ctx context.Context, body []byte) (*http.Response, error) {
	for i := 0; ; i++ {
		req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.serpkite.com/v1/search", bytes.NewReader(body))
		req.Header.Set("Authorization", "Bearer "+os.Getenv("SERPKITE_API_KEY"))
		req.Header.Set("Content-Type", "application/json")
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		retry := res.StatusCode == 429 || res.StatusCode >= 500
		if !retry || i >= 4 {
			return res, nil
		}
		res.Body.Close()
		wait, _ := strconv.Atoi(res.Header.Get("Retry-After"))
		if wait == 0 {
			wait = 1 << i
		}
		time.Sleep(time.Duration(wait)*time.Second + time.Duration(rand.Intn(500))*time.Millisecond)
	}
}
```

## Staying under the limit

- **Cap concurrency** in your client. At a typical latency of about a second, a limit of N requests per second means roughly N requests in flight at once.
- **Use the batch lane** for anything that doesn't need an answer right now. [`POST /v1/batches`](https://serpkite.com/docs/batch) takes up to 100 requests per call, costs half price, and you don't have to pace it.
- **Use the cache** for repeated queries. A `max_age` hit is half price and returns faster. See [Caching](https://serpkite.com/docs/caching).
- **Split workloads across keys.** Each key has its own per-second budget.

Need more than 100 requests per second? [Talk to us about Enterprise](https://serpkite.com/enterprise).

## Playground limits

The no-login demo on the [playground](https://serpkite.com/playground) doesn't use a key, so it is limited per IP address instead: at most 5 searches per minute and 20 per day. Past either limit it answers `429 rate_limited`. Playground searches are never billed and don't count against your key's limit. For anything beyond a quick look, [create a free key](https://app.serpkite.com/login?signup=1).

## Related

- [Errors](https://serpkite.com/docs/errors): All error codes, and which ones to retry.
- [Batch requests](https://serpkite.com/docs/batch): Up to 100 queries per call on the half-price batch lane.
- [Credits and billing](https://serpkite.com/docs/credits-and-billing): Packs and the rate each one unlocks.
- [Response headers](https://serpkite.com/docs/response-headers): Retry-After and the credit headers.