API Rate Limits
Every request to api.openstatus.dev is counted against a small set of limits. They exist to keep one client from degrading the API for everyone else; a well-behaved integration will never notice them.
Limits
| Scope | Limit | Applies to |
|---|---|---|
| Per API key or token | 600 requests per minute | all authenticated requests |
| Per API key or token | 100 requests per 10 seconds | all authenticated requests |
| Per API key or token | 60 write requests per minute | mutations on /v1, /rpc and /oauth (see below) |
| Per client IP | 120 requests per minute | unauthenticated /public/* status endpoints |
| Per client IP | 600 failed authentications per minute | requests that present a credential and get a 401, across all credentials |
Requests are attributed to the x-openstatus-key header, or to the OAuth bearer token when there is no key. Requests that carry neither are attributed to the client IP.
A write is any POST, PUT, PATCH, or DELETE on /v1 or /oauth, and any RPC on /rpc whose method does not start with Get, List, or Check. CreateMonitor, DeleteStatusReport, TriggerMonitor, and SendTestNotification are writes; ListMonitors and GetStatusPage are not. OAuth registration, token and revocation requests usually carry no credential, so their write budget is per client IP.
The failed-authentication budget only counts 401 responses, so a client sharing an egress IP with others is never charged for their traffic. Once it is exhausted, every request from that IP that carries a credential is rejected until the window resets.
All requests count, including the ones that fail with a 4xx. The health check /ping, the OpenAPI documents, and the OAuth discovery endpoints under /.well-known/ are exempt.
Note
Limits are enforced per API server instance, so the numbers above are approximate. A client whose requests are spread across several servers gets slightly more headroom, never less.
Rate limited response
A request over a limit is rejected before it reaches the route. The response is 429 Too Many Requests with a Retry-After header in seconds and the standard error envelope:
HTTP/1.1 429 Too Many Requests
Retry-After: 7
Content-Type: application/json
{
"code": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded, retry later",
"docs": "https://www.openstatus.dev/docs/api-references/errors/code/TOO_MANY_REQUESTS",
"requestId": "1c4b8f1e-3f4b-4f79-9f9d-2a6d7f0b6c1a"
}
On /rpc the body is a Connect error instead, so ConnectRPC clients such as the Node.js SDK raise a ConnectError with code resource_exhausted:
{ "code": "resource_exhausted", "message": "Rate limit exceeded, retry later" }
When a server is overloaded it may also answer 503 Service Unavailable with a Retry-After header: code: "SERVICE_UNAVAILABLE" in the envelope, unavailable on /rpc. Treat it exactly like a 429.
Retrying
- Honour
Retry-After. Wait at least that many seconds before sending the next request. The header is always present on429and503. - Back off exponentially on repeated rejections and add jitter so several workers do not retry in lockstep.
- Do not retry in a tight loop. A client that retries immediately, without a timeout, stays rate limited for the whole window and starves its own successful requests.
- Batch reads. Use the
List*RPCs and/v1list endpoints instead of fetching resources one by one. - Cache status. Public status endpoints change rarely; poll them at most every few seconds, not on every page view.
async function withRetryAfter(fn: () => Promise<Response>): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const res = await fn();
if ((res.status !== 429 && res.status !== 503) || attempt === 5) return res;
const seconds = Number(res.headers.get("retry-after") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, seconds * 1000 + Math.random() * 250));
}
}
Need more?
The limits are sized well above what any workspace uses today. If your integration legitimately needs more, contact ping@openstatus.dev with your workspace slug and the request pattern.