Skip to Content

Rate limits

The Regulatory Snapshot API rate-limits per API key. Limits are bucketed by category (reads, scans, exports) — heavy reads don’t eat into your scan budget, and vice versa. Limits return draft-7 RateLimit-* headers on every response, plus Retry-After when you cross the line.

This page lists the buckets, the headers we set, and the conventional retry pattern. If you’ve configured the SDKs, you can skim — they handle 429s for you.

The buckets

BucketLimitWindowApplies to
reads601 minuteGET /v1/snapshots/*, GET /v1/scans/:id, GET /v1/exports/:id, GET /v1/usage
scans61 hourPOST /v1/scans
exports301 hourPOST /v1/exports
key_mgmt101 minute/v1/keys/* (dashboard, per-user)

All buckets are per API key (except key_mgmt, which is per Clerk user — keys aren’t relevant on the key-management surface itself).

[!TIP] Need more headroom? Spinning up additional keys does not raise your aggregate scan or export budget — limits scale linearly with the number of active keys, but budget (cents/USD) is per-account. For an architecture serving many tenants, sharding by API key per tenant is the conventional pattern.

The SSE stream (GET /v1/scans/:id/stream) is not rate-limited; one long-lived stream per scan is the expected pattern.

Response headers

Every response carries draft-7 RateLimit-* headers (IETF draft ):

RateLimit-Limit: 60 RateLimit-Remaining: 41 RateLimit-Reset: 38
  • RateLimit-Limit — total requests allowed in the current window.
  • RateLimit-Remaining — how many are left.
  • RateLimit-Reset — seconds until the window resets.

When you cross the limit, the response is:

HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 60 { "type": "https://api.regsn.app/problems/rate_limited", "title": "Rate limit exceeded", "status": 429, "code": "rate_limit_exceeded", "detail": "Rate limit exceeded for reads bucket. Retry after 60s.", "request_id": "req_…" }

The Retry-After header is in seconds. It is conservative (the full window length), not a precise reset time — wait at least that long.

How to handle 429s

The conventional pattern is:

  1. Look at Retry-After (or, equivalently, RateLimit-Reset).
  2. Sleep that many seconds.
  3. Retry the same request.

If you’re sending an Idempotency-Key on a POST (you should be — see Idempotency), retrying with the same key is safe: the server will either replay the cached response or actually re-run, depending on whether the original made it through.

while true; do resp=$(curl -s -o body -w '%{http_code}' \ https://api.regsn.app/v1/scans \ -H "Authorization: Bearer $REGSN_API_KEY" \ -H "Idempotency-Key: $IDEM" \ -H "Content-Type: application/json" \ -d '{"jurisdictions":["UK"],"areas":["AML"],"horizon":12}') if [[ "$resp" != "429" ]]; then break; fi retry=$(grep -i '^retry-after:' headers | awk '{print $2}' | tr -d '\r') sleep "${retry:-30}" done

Both SDKs do this automatically — up to 3 retries with exponential backoff and Retry-After honoring.

Why the limits are what they are

  • reads = 60/min is comfortably above any “render a dashboard” workload but stops accidental loops dead.
  • scans = 6/hour matches the cost reality — a scan is the expensive primitive. If you need more, mode=async and a queue is the right shape, not a higher rate limit.
  • exports = 30/hour lets you fan out a single snapshot to multiple artifacts (PDF + tearsheet + audio + deck = 4 exports) several times per hour.

If you’re hitting limits regularly, two questions to ask:

  1. Are you polling too aggressively? A 5-second poll on GET /v1/scans/{id} is plenty.
  2. Should you be using SSE instead? /v1/scans/{id}/stream doesn’t count against any bucket.

Diagnosing hot keys

/v1/usage?group_by=key shows you per-key request counts and costs. If one key is dominating, that’s probably the one hitting 429s — consider sharding the workload by tenant or by use-case so each tenant gets its own bucket.

[!TIP] Pair Idempotency-Key with your retry-after handling. Retrying a POST with the same idempotency key after a 429 is safe — the server either replays the cached response (if the first attempt did make it through) or queues a fresh job. Without an idempotency key, a 429-then-success retry can double-bill you.

See also

  • Errors — full problem-details schema; 429 rate_limit_exceeded.
  • Idempotency — pair idempotency keys with retries to keep 429 retries safe.
  • Usage — group_by=key to find hot keys.
  • Glossary — rate limit, idempotency key.