---
title: "Rate limits"
url: "https://lookvalid.com/docs/rate-limits.md"
canonical_url: "https://lookvalid.com/docs/rate-limits"
last_updated: "2026-10-22"
type: "reference"
summary: "The request limit on each API key, the headers that report it and what to do on an HTTP 429."
related: ["https://lookvalid.com/docs/errors.md"]
---

# Rate limits

The request limit on each API key, the headers that report it and what to do on an HTTP 429.

Limits protect the service and keep one noisy client from slowing the rest. They apply per API key, and rejected requests are never charged.

## The limit

The default limit is **200 requests per 10 seconds per key**. It is a token bucket: a key can burst up to 200 requests at once, and the allowance refills continuously across the window rather than resetting on the clock.

On top of that, an account can have at most **16 lookups running at once** across all its keys. Cached results return immediately and do not count toward that cap.

## Headers

Every authenticated response carries the state of the key's bucket:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per window for this key. |
| `X-RateLimit-Remaining` | Requests left right now. |
| `X-RateLimit-Reset` | Unix time in seconds when the bucket is full again. |
| `Retry-After` | Seconds to wait before retrying. Sent only on a 429. |

Lookup responses also carry `X-Request-Id`, `X-Credits-Charged` and `X-Credits-Remaining`. A request with a bad key is rejected before the limiter, so a 401 has no rate limit headers.

## When you hit the limit

The API answers HTTP 429 with the [`rate_limited`](https://lookvalid.com/docs/errors.md#rate_limited) error and a `Retry-After` header. Wait that many seconds, then retry. The error is retryable and nothing was charged.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests for this API key.",
    "fix": "Wait the number of seconds in the Retry-After header, then retry. Spread requests out to stay under the X-RateLimit-Limit value.",
    "docs_url": "/docs/errors#rate_limited",
    "retryable": true,
    "request_id": "req_..."
  }
}
```

## Retry with backoff

**Node.js**

```js
async function lookup(path, body, attempts = 5) {
  for (let attempt = 1; ; attempt += 1) {
    const response = await fetch(`https://YOUR_API_HOST${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.LOOKVALID_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });
    if (response.status !== 429 || attempt === attempts) return response;
    const wait = Number(response.headers.get("Retry-After") ?? 1);
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}
```

**Python**

```python
import os
import time
import requests

def lookup(path, body, attempts=5):
    for attempt in range(1, attempts + 1):
        response = requests.post(
            f"https://YOUR_API_HOST{path}",
            headers={"Authorization": f"Bearer {os.environ['LOOKVALID_API_KEY']}"},
            json=body,
            timeout=30,
        )
        if response.status_code != 429 or attempt == attempts:
            return response
        time.sleep(float(response.headers.get("Retry-After", 1)))
```

## Staying under the limit

- Spread bursts out instead of firing every request at once.
- Read `X-RateLimit-Remaining` and slow down before it reaches zero.
- Do not repeat lookups you already have: cached repeats are free in credits, but they still count toward the request limit.
- Higher limits are not self-serve yet. If you need more, email [hello@lookvalid.com](mailto:hello@lookvalid.com) with your use case.

## Related

- [Errors](https://lookvalid.com/docs/errors.md)
