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.
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.
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
| Header | Meaning |
|---|---|
x-ratelimit-limit | the window size you asked for |
x-ratelimit-remaining | requests left in the current window β watch it burn down |
x-ratelimit-reset | epoch seconds when the window rolls over (countdown-UI fuel) |
retry-after | on 429s: live seconds until the reset β never more than 60 |
x-mockbird-ratelimit: simulated | so 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.
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.
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=429 | mock_ratelimit=N | |
|---|---|---|
| What it tests | the single-response render path | the whole flow: burn-down β block β wait β recover |
| State | none β every request is a 429 | live per-IP counter with a rolling window |
| Headers | status only | limit / remaining / reset + live Retry-After |
| Good for | unit tests, Storybook states | retry loops, queue workers, countdown UI, integration tests |
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}'
N values, so changing mock_ratelimit=5 to =100
mid-window doesn't reset what you've used.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.
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.