How to handle 429 rate limits properly
A 429 is not an error in the usual sense. Nothing broke, nothing is misconfigured, and retrying immediately is the one response guaranteed to make it worse. It is a scheduling instruction, and the correct client reads it as one.
Read the headers before you write a retry loop
A well-behaved API tells you your budget on every response, not only when you exceed it. Checking those headers on successful calls lets you slow down before hitting the wall, which is far better than recovering after.
RateLimit-LimitRateLimit-RemainingRateLimit-ResetRetry-AfterBackoff, with jitter, and a cap
When there is no Retry-After, double the wait each attempt and add randomness. The doubling gives the server room to recover. The randomness stops every client that failed at the same moment from returning at the same moment, which is how a brief limit becomes a sustained outage.
async function call(req, attempt = 0) {
const res = await fetch(req);
if (res.status !== 429) return res;
if (attempt >= 5) throw new Error('rate limited, giving up');
const after = Number(res.headers.get('Retry-After'));
const wait = Number.isFinite(after) && after > 0
? after * 1000
: Math.min(2 ** attempt * 1000, 30000) * (0.5 + Math.random());
await new Promise(r => setTimeout(r, wait));
return call(req, attempt + 1);
}
Three details matter more than the formula. Cap the delay, or attempt eight waits four minutes and your job looks hung. Cap the attempts, because a limit that persists past five tries is a design problem, not a transient one. And never retry a 4xx that is not 429 or 503; a 422 will fail identically forever.
Agents are the worst offenders
A language model given a tool that returns an error will usually try again. Immediately. With a slightly reworded argument. It has no concept of a token bucket, and a 429 message reads to it like a hint to rephrase rather than to wait.
This is why a server-side MCP implementation should absorb the pacing rather than pass it upward. When c1k.me's MCP endpoint is throttled it waits and completes the call, and only surfaces an error if the wait would be unreasonable. When it does surface one, the message says how many seconds to wait in plain language, because that is the only form an agent reliably acts on.
Not hitting the limit in the first place
Published limits are a feature
Plenty of APIs describe their limits as dynamic, adaptive, or subject to change without notice. In practice that means you discover them by being blocked, usually in production, usually at the worst moment. You cannot design a client against a number nobody will tell you.
Sixty reads and ten writes a minute is not generous. It is written on the pricing page, it is the same for every customer, and it will not quietly tighten when load grows. A modest published number beats a large secret one, because only the published one lets you write a client that behaves correctly by construction.