ShieldSignup
Guides

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

PlanRequests per secondMonthly assessments
Free11,000
Starter1010,000
Pro50100,000
Business2001,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
HeaderMeaning
X-RateLimit-LimitPer-second request limit for this key.
X-RateLimit-RemainingApproximate requests still available in the current 1-second window.
X-RateLimit-ResetUnix epoch second at which the per-second window resets.
X-Quota-UsedLive assessments deducted from quota so far this month (sandbox calls do not count).
X-Quota-LimitMonthly 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.codeWhenWhat to do
rate_limitPer-second burst budget exhausted.Sleep until X-RateLimit-Reset, then retry — usually less than a second.
quota_exceededMonthly 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:00Z on the first day of each UTC month.
  • Sandbox keys (sk_test_…) never deduct quota.
  • When quota is exhausted, the API returns 429 with error.code = "quota_exceeded" and X-Quota-UsedX-Quota-Limit.
  • Watch GET /v1/usage or your dashboard's Overview page to know when you're approaching the cap.

On this page