Rate limits
Per-second burst limits, monthly quota, response headers, and how to back off correctly when you hit a 429.
Every plan has both a per-second burst limit and a monthly quota. The two
are enforced separately and each surface its own error code on a 429.
Limits by plan
| Plan | Requests per second | Monthly assessments |
|---|---|---|
| Free | 1 | 1,000 |
| Starter | 10 | 10,000 |
| Pro | 50 | 100,000 |
| Business | 200 | 1,000,000 |
Business is sales-assisted; everything else is self-serve from
Dashboard → Settings → Billing.
Burst behaviour
The per-second limit is enforced on a sliding 5-second window with a 3× burst budget. In practice:
- A Starter key (10 rps) can spike to 150 requests over 5 seconds — i.e. up to 3× the rps for a 5-second slice — before being rate-limited.
- After a burst, the window resets gradually as time advances.
This is generous enough that batch back-fills, marketing-email-driven
spikes, and synthetic load tests usually do not need to throttle on the
client side. Production traffic should rarely hit rate_limit.
Rate-limit headers
Every successful response carries five headers that describe your current position against both the per-second and the monthly limits.
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1717200000
X-Quota-Used: 4312
X-Quota-Limit: 10000| Header | Meaning |
|---|---|
X-RateLimit-Limit | Per-second request limit for this key. |
X-RateLimit-Remaining | Approximate requests still available in the current 1-second window. |
X-RateLimit-Reset | Unix epoch second at which the per-second window resets. |
X-Quota-Used | Live assessments deducted from quota so far this month (sandbox calls do not count). |
X-Quota-Limit | Monthly cap for this plan. |
Two flavours of 429
There are two error.code values on a 429. They mean different things
and your handling should differ:
error.code | When | What to do |
|---|---|---|
rate_limit | Per-second burst budget exhausted. | Sleep until X-RateLimit-Reset, then retry — usually less than a second. |
quota_exceeded | Monthly cap reached. Only emitted for sk_live_… keys. | Upgrade your plan, wait for the next UTC month reset, or fall back to your fail-open policy. Retrying immediately will keep failing. |
Retry with backoff
Always set a client-side timeout and a retry budget. Limit retries to two or three at most — runaway retry loops are the most common cause of self-inflicted outages.
async function assessWithRetry(
email: string,
ip: string,
retries = 2,
): Promise<Response> {
const res = await fetch("https://api.shieldsignup.com/v1/assess", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SHIELDSIGNUP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, ip }),
signal: AbortSignal.timeout(3000),
});
if (res.status !== 429 || retries === 0) {
return res;
}
// Sniff the error code so we don't retry a quota_exceeded.
const body = (await res.clone().json().catch(() => null)) as
| { error?: { code?: string } }
| null;
if (body?.error?.code === "quota_exceeded") {
return res;
}
const reset = res.headers.get("X-RateLimit-Reset");
const waitMs = reset ? Math.max(parseInt(reset) * 1000 - Date.now(), 200) : 500;
await new Promise((resolve) => setTimeout(resolve, waitMs));
return assessWithRetry(email, ip, retries - 1);
}import os
import time
import requests
URL = "https://api.shieldsignup.com/v1/assess"
def assess_with_retry(email: str, ip: str, retries: int = 2) -> requests.Response:
response = requests.post(
URL,
headers={
"Authorization": f"Bearer {os.environ['SHIELDSIGNUP_API_KEY']}",
"Content-Type": "application/json",
},
json={"email": email, "ip": ip},
timeout=3,
)
if response.status_code != 429 or retries == 0:
return response
# Don't retry quota_exceeded — it won't recover for the rest of the month.
body = response.json().get("error", {}) if response.content else {}
if body.get("code") == "quota_exceeded":
return response
reset = response.headers.get("X-RateLimit-Reset")
wait = 0.5
if reset:
wait = max(int(reset) - time.time(), 0.2)
time.sleep(wait)
return assess_with_retry(email, ip, retries - 1)Monthly quota mechanics
- Quota counts only successful
sk_live_…assessments. Errors before the scorer runs (validation, auth) do not deduct. - Quota resets at
00:00:00Zon the first day of each UTC month. - Sandbox keys (
sk_test_…) never deduct quota. - When quota is exhausted, the API returns
429witherror.code = "quota_exceeded"andX-Quota-Used≥X-Quota-Limit. - Watch
GET /v1/usageor your dashboard's Overview page to know when you're approaching the cap.