# Rate Limits

> Current Query API limits and retry behavior


import { Callout, CodeBlock } from "@/components/docs";

Rate limits protect the service from bursts and are applied by endpoint and authenticated identity. They are not currently plan-based.

## Query API limits

| Endpoint | Authenticated limit | Anonymous limit | Window |
|----------|---------------------|-----------------|--------|
| `POST /v1/query/compile` | 300 requests | 60 requests | 1 minute |
| `POST /v1/query` | 120 requests | 60 requests | 1 minute |

An API key, signed-in user, or anonymous client receives a separate limit identity. Other API families can apply their own limits.

## API key admission limits

Independent of the per-endpoint limits above, every API key request passes an admission layer:

- **Global per-key limit**: when rate limiting is enabled on the key, a rolling quota applies across all endpoints (default 300 requests per 60 seconds; a custom limit can be stored on the key).
- **Concurrency cap**: at most 20 in-flight requests per key. Exceeding it returns `429` with the message "Too many concurrent API key requests" and `Retry-After: 1`.
- **Degraded infrastructure**: if the admission infrastructure is unavailable, requests return `503` with code `SERVICE_UNAVAILABLE` and `Retry-After: 5`.

## Event ingestion limit

`POST /track` on `basket.databuddy.cc` is limited to 600 requests per 60 seconds per API key or website+IP.

## When a limit is reached

A `429` response includes the retry window in both headers and JSON:

<CodeBlock
  language="http"
  code={`HTTP/1.1 429 Too Many Requests
Retry-After: 8
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735732810000
X-Request-ID: req_abc123def456`}
/>

<CodeBlock
  language="json"
  code={`{
  "success": false,
  "error": "Rate limit exceeded",
  "code": "RATE_LIMITED",
  "requestId": "req_abc123def456",
  "limit": 120,
  "remaining": 0,
  "reset": 1735732810000,
  "retryAfter": 8
}`}
/>

`Retry-After` and `retryAfter` are seconds. `X-RateLimit-Reset` and `reset` are Unix timestamps in milliseconds. Rate-limit headers are guaranteed on the `429` response; do not assume they are present on successful responses.

## Retry safely

<CodeBlock
  language="typescript"
  code={`async function fetchWithRateLimitRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
    if (response.status !== 429 || attempt === maxRetries) return response;

    const retryAfter = Number(response.headers.get("Retry-After") ?? 1);
    const jitter = Math.floor(Math.random() * 250);
    await new Promise((resolve) =>
      setTimeout(resolve, Math.max(1, retryAfter) * 1000 + jitter)
    );
  }
}`}
/>

Batch compatible analytics parameters into one `/v1/query` request and cache historical results when appropriate.

<Callout type="info">
  If a production integration needs a different traffic profile, [contact support](mailto:support@databuddy.cc) with the endpoint, expected peak rate, and request IDs from any `429` responses.
</Callout>
