Rate limits
The API is rate limited to keep it responsive for everyone. Limits are applied per tenant
and per token, reported on every response, and signalled with 429 Too Many Requests when
exceeded.
How limiting works
Every request is counted against one or more buckets. Each bucket has a fixed window that starts on the first request and resets one minute later.
| Bucket | Applies to | Default limit |
|---|---|---|
| Per tenant | All requests from your organisation, whatever the token | 2,000 requests per minute |
| Per token | Requests made with a single API token | 600 requests per minute |
| Per IP address | Unauthenticated requests | 60 requests per minute |
Buckets are evaluated together and the tightest one wins. A single token cannot exceed 600 requests per minute even when your tenant has headroom, and the tenant ceiling still applies across all of your tokens combined.
Limits vary by plan
The figures above are the platform defaults. Your effective limits depend on your plan — see pricing or contact us for enterprise ceilings. Always read the response headers rather than hard-coding a number.
Response headers
Every rate-limited response carries the current bucket state, following the IETF
RateLimit header conventions:
RateLimit-LimitintegerOptionalThe maximum number of requests permitted in the current window.
RateLimit-RemainingintegerOptionalRequests still available in the current window. When this reaches 0, further requests
return 429 until the window resets.
RateLimit-ResetintegerOptionalSeconds remaining until the current window resets and the counter returns to the full limit.
Retry-AfterintegerOptionalSent only on a 429 response. The number of seconds to wait before retrying.
Prefer this value over your own backoff calculation.
These headers are exposed to browsers via CORS, so a front-end calling the API directly can read them too.
curl -i https://api.ospulse.app/api/v1/packages?take=1 \
-H "Authorization: Bearer $OSPULSE_TOKEN" | grep -i "ratelimit"RateLimit-Limit: 600
RateLimit-Remaining: 597
RateLimit-Reset: 42When you are limited
Exceeding a bucket returns 429 with a standard problem details body:
{
"title": "Too many requests",
"status": 429,
"detail": "The rate limit for this caller has been reached. Retry after 42 seconds."
}The Retry-After header accompanies the response. Waiting for that many seconds is always
sufficient — retrying sooner will simply be rejected again.
Endpoints that are never limited
A small set of paths are exempt so that health checking and event ingest never fail under load:
/api/v1/healthand/api/v1/version— liveness and readiness probes/api/v1/webhooks/…— inbound source-control ingest, which is rate limited by the sending provider instead/api/v1/status/…— the public status surface
You can poll these freely, which makes /api/v1/health a safe connectivity check.
Handling limits correctly
The single most important rule: respect Retry-After and back off exponentially with
jitter. Retrying immediately, or on a fixed interval, tends to synchronise all your
workers into a thundering herd that keeps you permanently limited.
async function request(path, init = {}, attempt = 0) {
const res = await fetch(`https://api.ospulse.app${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.OSPULSE_TOKEN}`,
...init.headers,
},
})
if (res.status === 429 && attempt < 5) {
// Trust the server's own guidance, then add jitter so parallel
// workers do not all wake at the same instant.
const retryAfter = Number(res.headers.get('retry-after') ?? 1)
const jitter = Math.random() * 500
await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter))
return request(path, init, attempt + 1)
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
return res.json()
}Staying well under the limit
Most integrations that hit rate limits are doing more work than they need to.
- Page with larger
takevalues. Fetching 100 items per request instead of 10 uses a tenth of the requests. See pagination. - Poll on a sensible cadence. Scan results do not change second by second. Poll a running scan every few seconds, not continuously.
- Cache what is stable. Package metadata and health scores change on the order of hours, not milliseconds.
- Use one token per integration. Separate tokens get separate per-token buckets, so a chatty CI job cannot starve your dashboard. It also makes revocation surgical — see authentication.
- Prefer webhooks to polling where an event-driven flow is available.
Watch RateLimit-Remaining
Rather than waiting to be rejected, read RateLimit-Remaining on responses you already
receive and slow down as it approaches zero. Well-behaved clients rarely see a 429 at all.
