Skip to content

Rate Limits ​

There is one enforced limit, and it is narrower than a tier table. This page states what is enforced, what the headers mean and how to behave around a 429. An earlier edition described public and authenticated tiers of 100 and 500 requests a minute; no such tiers exist and no credential raises any limit.

What is enforced ​

A single Cloudflare edge rule, applied before a request reaches the API:

PathsLimit
Every path beginning /api/v1/sbom (/sbom, /sbom/scan, /sbom/time-travel, /sbom/formats) and /api/v1/cve/:id/ai-assessment2 requests per 10 seconds per address, enforced per Cloudflare location

These are the routes where one call fans out to an upstream per component, or runs a model. The counter is shared across the paths listed, so two SBOM scans and an AI assessment inside ten seconds from one address is one request too many. The same figure is published by GET /api/v1/sbom/formats under limits.perEndpoint, and a test binds that advertisement to the rule.

A request the edge rule blocks answers 429 from Cloudflare itself, ahead of the API, so its body is not the JSON envelope below. Plan on the status code.

Every other route runs without an enforced per-address budget. Cached answers never reach the counter, so repeating a request that answered X-Cache: HIT costs nothing.

The headers ​

Responses under /api/ carry three headers:

X-RateLimit-Limit: 30
X-RateLimit-Remaining: 26
X-RateLimit-Reset: 1790557390

They come from a counter inside the API worker instance that served the request. That instance is one of many, each with its own counter, so the figures describe a burst that landed on one instance and are not an account-wide budget. Treat them as advisory: a falling Remaining means you are sending quickly, and reaching zero on one instance answers 429 from that instance with Retry-After set, but a Remaining of 26 is not a promise of 26 more requests. The Limit shown is the per-instance figure for the route's bucket: 30 a minute for most routes, 60 for /issues, 15 for /search and /trending, 10 for POST /cve/status, 20 for /prefetch/* and 500 for the vendor-hash chunk operations.

Repeated requests for paths that do not exist are counted the same way, and an instance that sees more than ten in a minute from one address answers 403 with no body to the rest.

Rate Limit Exceeded ​

A 429 from the API itself looks like this:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Cache-Control: no-store

{
  "error": "Too many requests",
  "retryAfter": 60
}

Honour Retry-After. A 429 from the edge rule carries Cloudflare's own body; wait ten seconds before the next call to the paths it covers.

Best Practices ​

Implement Exponential Backoff ​

javascript
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url);

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get('Retry-After')) || 10;
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000 * (i + 1)));
      continue;
    }

    return response;
  }
  throw new Error('Max retries exceeded');
}

Cache Responses ​

Most vulnerability data changes infrequently. Cache responses locally:

  • Stats: an hour is plenty; the server refreshes every two
  • Issues: an hour
  • OSV/Vulnerabilities: 15 minutes
  • Repology: two hours, matching the server

Batch Requests ​

Prefer the routes that answer for many identifiers in one call over a loop of single lookups: POST /api/v1/cve/batch for up to 50 CVEs, POST /api/v1/cve/status for status badges, GET /api/v1/packages/index for membership checks. Where a loop is unavoidable, space it:

bash
for pkg in openssl curl nginx; do
  curl "https://api.vulnpatch.dev/api/v1/osv/$pkg"
  sleep 1
done

Caching ​

The API caches server-side with these TTLs:

EndpointCache TTL
/stats2 hours
/issues1 hour
/nix/cves, /nix/channels1 hour
/nix/repology/:package2 hours
/nix/hydra/:package4 hours
/ecosystem-stats24 hours
/recent-advisories10 minutes
/search30 minutes
/cve/:id2 hours

Cache status is indicated in the X-Cache header:

  • X-Cache: HIT - Response served from cache
  • X-Cache: MISS - Fresh response generated

Most GET answers also carry a short Cache-Control: public, max-age so an intermediary can hold them; errors and degraded answers are no-store.