Rate limits
How many requests an API key may send, how limits are counted and what happens when one is reached.
PilotFox limits requests per credential, so that one integration cannot slow down everyone else. The limits are built into PilotFox; the few that a self-hosted instance can change name their setting.
Budgets per credential
Every endpoint under /api/v1 counts against the budget of the credential that calls it:
| Requests | Budget |
|---|---|
Reads (GET, HEAD) | 600 a minute |
Writes (POST, PUT, PATCH, DELETE) | 60 a minute |
Reads and writes are separate budgets. Each one is counted in windows of one minute, and every request counts,
whatever its answer: a request refused with 400 or 403 uses the budget too.
Who a budget belongs to:
- an API key: each key has its own budgets, so two keys of one organization do not share them (an organization holds at most 50 active keys);
- a dashboard user: one budget per person, across their browser tabs;
- a share link: one budget per link, shared by everyone who opens it;
- anonymous callers (readers of a public site): one budget
per client address, where an IPv6
/64counts as one address.
Server-side ingest is a write: an ingest key can send 60 requests a minute, each with up to 500 events.
Endpoints with their own budget
A few endpoints have a budget of their own, which they do not share with the general one:
| Endpoint | Budget |
|---|---|
POST /api/v1/sites/{siteId}/payments, and the payment refund and delete calls | 300 a minute per key, for each of the three |
POST /api/v1/sites/{siteId}/verify-install | 10 a minute |
GET /api/v1/sites/{siteId}/favicons/{domain} | 60 a minute |
GET /api/v1/affiliate-portal | 60 a minute per client |
GET /api/v1/health is not limited. The realtime stream
(GET /api/v1/sites/{siteId}/live/stream) is limited by open
connections instead of requests, on each server: at most 5 open streams per credential, 100 per organization by
default (LIVE_MAX_STREAMS_PER_ORG on a self-hosted instance), and 20 per person across all organizations by default
(LIVE_MAX_STREAMS_PER_OWNER), counting their own streams and those of the API keys they created. Share links and
readers of a public site count against the site instead, 50 streams per site by default
(LIVE_MAX_PUBLIC_STREAMS_PER_SITE). A stream past a limit is refused with 429 and Retry-After: 5.
The MCP server
The MCP server at /api/mcp allows 120 requests a minute per credential (an API key, or an OAuth
token's user and client together) and answers a JSON-RPC error with status 429 past it. The tools it runs call the
REST endpoints as the same credential, so each tool call also counts against that credential's REST budget.
When a limit is reached
The request is answered 429 with the standard error envelope, a Retry-After header and the same number of seconds
in details.retryAfterSeconds:
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 60
x-ratelimit-remaining: 0
x-ratelimit-reset: 59
retry-after: 59
content-type: application/json; charset=utf-8
{"error":{"code":"rate_limited","message":"Too many requests","details":{"retryAfterSeconds":59}}}Every limited response, successful or not, carries the budget headers:
| Header | Meaning |
|---|---|
x-ratelimit-limit | The budget of this kind of request (600 for reads, 60 for writes, or the endpoint's own). |
x-ratelimit-remaining | Requests left in the current window. |
x-ratelimit-reset | Seconds until the window ends. |
Wait for Retry-After seconds before sending again. A client that keeps an eye on x-ratelimit-remaining can slow
down before it runs out.
Refused credentials
Each client address (an IPv6 /64 counts as one) may present 60 refused credentials a minute: unknown, expired or
revoked API keys, share tokens and affiliate portal tokens. Past that, for the rest of the minute, every API key or share token it sends is
answered 429 without being checked, valid ones included, unless PilotFox verified that key in the last minute. A
misconfigured job retrying a revoked key in a loop can therefore lock out the other jobs on the same machine for a
minute: stop on 401 instead of retrying.
Heavy queries and exports
Some limits are not counted per minute:
- Concurrent analytics queries. Each credential runs at most 4 database queries at a time on each server; further
queries wait their turn. A query that runs longer than 30 seconds (
QUERY_MAX_EXECUTION_SECONDSon a self-hosted instance), or that has not finished 10 seconds after that limit counting its wait, is answered504. Running many reports in parallel with one key does not make them faster than about four at a time. - Exports. One raw-event export runs at a time per organization by default (
EXPORT_MAX_CONCURRENT_PER_ORG); a second one is429until the first finishes. When the server is running its maximum of exports (2 by default,EXPORT_MAX_CONCURRENT), a new one is503. - Imports. An organization may stream 2 import uploads at a time by default (
IMPORT_MAX_UPLOADS_PER_ORG; one more is429) and hold 3 imports queued or running (IMPORT_MAX_PENDING_PER_ORG; one more is409). - Query size. A report whose result or memory use is too large is a
400asking you to narrow the window or the filters, and a query string over 16 KB is refused with431.
Tracking script endpoints
The endpoints the tracking script sends to are not under /api/v1 and are limited per visitor
address instead. Events and identify calls (/api/e, /api/i) share a token bucket of 30 requests a second with a
burst of 120 by default (INGEST_RATE_PER_IP on a self-hosted instance); the site configuration the script fetches
(/api/c/…) has a bucket twice that size, and replay uploads one of their own. An IPv6 /64 counts as one address. A
limited request is answered 429. Behind a first-party proxy, each visitor's address keeps
its own bucket, counted within that site only, and the proxy's own address has a bucket 100 times larger.