writing/12 Sep 2026/7 min read

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-Limit
Requests allowed in the current window. On c1k.me, 60 per minute per credential, and 10 per minute for mutations.
RateLimit-Remaining
What is left. Below roughly ten percent, start pacing rather than sprinting.
RateLimit-Reset
Seconds until the window refills.
Retry-After
Present on the 429 itself. This is the server telling you exactly how long to wait. Honour it and most of the complexity below disappears.

Backoff, 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.

If you build agent tooling: never return a bare status code to a model. Return a sentence saying what to do next. “Rate limited, retry in 30 seconds” produces correct behaviour; “HTTP 429” produces a loop.

Not hitting the limit in the first place

Page with a cursor
A hundred links is two requests at the default page size, not a hundred lookups. Fetch the list, then work locally.
Poll the change feed
One call every few minutes tells you what changed. Re-reading every object on a schedule is how a small integration becomes a rate limit problem.
Separate credentials per job
Limits are per credential. A bulk import running on its own key cannot starve your dashboard.
Space out bulk writes
Ten mutations a minute means importing 400 links takes about forty minutes. Plan for that rather than discovering it at attempt eleven.

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.