OSPulse

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.

BucketApplies toDefault limit
Per tenantAll requests from your organisation, whatever the token2,000 requests per minute
Per tokenRequests made with a single API token600 requests per minute
Per IP addressUnauthenticated requests60 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-LimitintegerOptional

The maximum number of requests permitted in the current window.

RateLimit-RemainingintegerOptional

Requests still available in the current window. When this reaches 0, further requests return 429 until the window resets.

RateLimit-ResetintegerOptional

Seconds remaining until the current window resets and the counter returns to the full limit.

Retry-AfterintegerOptional

Sent 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.

bash
curl -i https://api.ospulse.app/api/v1/packages?take=1 \
  -H "Authorization: Bearer $OSPULSE_TOKEN" | grep -i "ratelimit"
http
RateLimit-Limit: 600
RateLimit-Remaining: 597
RateLimit-Reset: 42

When you are limited

Exceeding a bucket returns 429 with a standard problem details body:

json
{
  "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/health and /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.

js
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 take values. 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.

Next steps