Test your rate-limit handling without getting banned β€” a mock API with a real 429 limiter

Every serious API rate-limits you, and every serious client is supposed to handle it: read Retry-After, back off, resume, maybe show the user a countdown. But how do you test that code? Hammering the real API until it throttles you is slow, flaky in CI, and with some providers gets your key or IP flagged. So most 429 handlers ship having never seen a 429.

Mockbird gives any mock endpoint a real, stateful rate limiter with one query parameter. Free, no signup, works on the public demo right now.

The 10-second version

for i in $(seq 1 7); do
  curl -s -o /dev/null -w "%{http_code} " \
    'https://mockbird.mockbird.workers.dev/m/demo/products?mock_ratelimit=5&limit=1'
done
200 200 200 200 200 429 429

?mock_ratelimit=5 means: 5 requests per 60-second window from your IP succeed, then you get genuine 429s until the window rolls over. This is not a canned response β€” it's a live counter, so a retry loop pointed at this URL experiences exactly what it would against GitHub or Stripe on a bad day.

The headers your client should be reading

Every response β€” successes too β€” carries GitHub-style headers:

$ curl -si 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_ratelimit=5&limit=1' \
    | grep -iE '^(HTTP|x-ratelimit|retry-after)'
HTTP/2 200
x-ratelimit-limit: 5
x-ratelimit-remaining: 4
x-ratelimit-reset: 1785452940

and once you're over the limit:

HTTP/2 429
retry-after: 19
x-ratelimit-limit: 5
x-ratelimit-remaining: 0
x-ratelimit-reset: 1785452880
HeaderMeaning
x-ratelimit-limitthe window size you asked for
x-ratelimit-remainingrequests left in the current window β€” watch it burn down
x-ratelimit-resetepoch seconds when the window rolls over (countdown-UI fuel)
retry-afteron 429s: live seconds until the reset β€” never more than 60
x-mockbird-ratelimit: simulatedso logs never confuse a drill with a real outage

The 429 body is JSON your error path can parse:

{
  "error": "simulated rate limit exceeded (mock_ratelimit=5 per 60s)",
  "retry_after": 5
}

All of the x-ratelimit-* headers are in Access-Control-Expose-Headers, so browser JavaScript can read them cross-origin β€” your countdown component works against this URL unchanged.

Recipe: prove your backoff actually backs off

async function fetchWithBackoff(url, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url);
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("retry-after") || 2 ** i);
    await new Promise(r => setTimeout(r, wait * 1000));
  }
  throw new Error("gave up after repeated 429s");
}

// point it at a tight limit and watch it recover:
const res = await fetchWithBackoff(
  "https://mockbird.mockbird.workers.dev/m/demo/products?mock_ratelimit=3&limit=1"
);

Call it in a loop: the first three sail through, the fourth gets a 429, sleeps for the real Retry-After, and succeeds in the next window. If your implementation ignores Retry-After and hot-loops, you'll see it immediately β€” a stack of 429s in the request inspector.

Static 429 vs. a stateful limiter

Sometimes you just need one response to be a 429 β€” for that, ?mock_status=429 forces it on any request, deterministically. The difference:

mock_status=429mock_ratelimit=N
What it teststhe single-response render paththe whole flow: burn-down β†’ block β†’ wait β†’ recover
Statenone β€” every request is a 429live per-IP counter with a rolling window
Headersstatus onlylimit / remaining / reset + live Retry-After
Good forunit tests, Storybook statesretry loops, queue workers, countdown UI, integration tests

It composes with everything

The limiter runs in front of the whole mock engine β€” reads, writes, custom routes, nested routes. Add mock_delay for slow-and-throttled, mock_chaos for throttled-and-flaky:

# slow AND rate-limited (2s latency, 10 req/min)
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_ratelimit=10&mock_delay=2000'

# writes count against the window too
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_ratelimit=10' \
  -H 'content-type: application/json' -d '{"name":"Widget","price":9.99}'

Honest notes

As far as we can tell (checked July 2026), no other free hosted mock-API tool ships a stateful limiter β€” static 429s are usually the best you can do, and WireMock Cloud puts fault injection behind its Enterprise tier. If you find one, tell us and we'll link it here.

Get your own (with your schema)

curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects \
  -H 'content-type: application/json' \
  -d '{"name":"my-api","preset":"ecommerce"}'

…or the one-click dashboard link. Every project gets the full simulation toolkit β€” delay, chaos, jitter, forced statuses, 422 validation, cursor pagination β€” plus GraphQL, snapshots and a request inspector. 10,000 requests/day free.