The first time you hit a rate limit, it feels like a betrayal. Your code was working perfectly. You ran it again to test something, and suddenly every request comes back 429 Too Many Requests. You wait a few minutes, run it again, and everything works. What just happened?
You met the quota that guards almost every public API. Learning to read what the server tells you is one of the most useful skills in building on third-party services.
What a rate limit actually is
A rate limit is a rule the provider enforces so no single client can overwhelm its infrastructure. The simple version is "X requests per Y seconds," but real limits come in layers. GitHub's REST API allows 60 requests an hour without authentication and 5,000 with a token. Stripe allows 100 requests per second in live mode and 25 per second on most individual endpoints. CoinGecko's free Demo key stacks two windows, 100 calls a minute and 10,000 a month, and the monthly one is usually what runs out.
Limits also depend on who you are. Wikimedia's new 2026 limits make the point: a request identified only by its IP address gets 10 per minute, while the same request with a descriptive User-Agent gets 200. Some providers don't count requests at all. Hyperliquid weights each call (a light price lookup spends 2 of the 1,200 weight allowed per minute), and AI APIs like Anthropic's meter input and output tokens per minute alongside requests.
Why rate limits exist
Infrastructure cost. Every call burns CPU, memory, database time and bandwidth. One runaway client can cost the provider real money and slow the service for everyone else.
Abuse prevention. Without limits, cloning an entire dataset takes minutes. Limits make bulk collection slow. Reddit took this to its logical end in May 2026 and shut off unauthenticated .json access entirely.
Fairness. One user sending 10 million requests an hour would crowd out everyone else. A limit guarantees each client a share. It isn't a punishment; it's how a shared resource stays available.
How servers actually count
Fixed window. Count requests per clock minute or hour and reset to zero at the boundary. It's cheap and easy to reason about, but a client can spend a full quota at 11:59:59 and another at 12:00:00, doubling the intended burst.
Sliding window. Smooths that edge by weighting the previous window's count by how much of it still overlaps the last 60 seconds. Cloudflare described this approach for its own rate limiting and tested it against 400 million requests: only 0.003% were wrongly allowed or blocked.
Token bucket. Each client gets a bucket that refills at a steady rate up to a maximum. Every request spends a token, and an empty bucket means a 429. You can burst up to the bucket size, then you're held to the refill rate. Anthropic's API documents that it works this way, and warns that 60 requests per minute may be enforced as one per second. Stripe recommends clients run their own token bucket to pace outgoing calls.
429, Retry-After and the headers
The standard over-limit response is HTTP 429, defined in RFC 6585 back in 2012. Servers may attach a Retry-After header, which RFC 9110 defines as either a number of seconds or an HTTP date. When it's present it outranks every other signal: wait that long. Some servers send it with a 503 during overload, too.
Not every 429 means "slow down," though. Stripe also returns 429 for object lock timeouts, and says a 429 without its Stripe-Rate-Limited-Reason header wasn't a rate limit at all. Anthropic returns 429 when an organization hits its monthly spend cap, deliberately without Retry-After, because retrying won't help until next month. A 429 with no Retry-After is a cue to read the body before looping.
Beyond that, header names are a mess. GitHub sends x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-used and an x-ratelimit-reset Unix timestamp. Reddit uses similar names, but its reset value is seconds from now. Discord adds X-RateLimit-Reset-After and an X-RateLimit-Bucket ID for the route you hit. Anthropic uses anthropic-ratelimit-* headers with RFC 3339 timestamps.
The IETF fix, still a draft
The IETF wants to fix this. The current version of its draft, draft-ietf-httpapi-ratelimit-headers-11, came out in May 2026 and is still a working group draft, not an RFC. It drops the X- prefix and defines two structured fields. This example is from the draft:
RateLimit-Policy: "fixedwindow";q=100;w=60
RateLimit: "fixedwindow";r=99;t=50
The policy gives the quota (q) per window (w, in seconds). The live field gives the units remaining (r) and the seconds left in the effective window (t). Relative seconds instead of timestamps sidestep clock-skew bugs. If a response carries these fields and Retry-After, the draft says Retry-After wins. Cloudflare's own API already sends Ratelimit and Ratelimit-Policy in this format, so it pays to parse both styles today.
The client patterns that actually work
Cache first. By far the most important. If a price changes every few seconds, fetch it once, store it and serve that copy to everyone. An API that allows 30 calls a minute can then serve any number of users.
Back off with jitter. On a 429 or 503, honor Retry-After when it's there. Otherwise wait exponentially longer after each failure, and randomize the wait: plain doubling (1s, 2s, 4s) makes every client that failed together retry together. AWS engineer Marc Brooker compared the options in a widely cited 2015 post and found "full jitter," a random wait between zero and the exponential cap, worked best. Only retry requests that are safe to repeat, and cap the attempts.
async function fetchWithBackoff(url, tries = 5) {
for (let i = 0; i < tries; i++) {
const res = await fetch(url);
if (res.status !== 429 && res.status !== 503) return res;
const ra = Number(res.headers.get('retry-after')); // seconds form only
const cap = Math.min(30000, 1000 * 2 ** i);
const wait = ra > 0 ? ra * 1000 : Math.random() * cap;
await new Promise(r => setTimeout(r, wait));
}
throw new Error('still rate limited after ' + tries + ' tries');
}
Batch. Many APIs accept several IDs per call. Most limits count requests, not items, so fetching 100 records in one request instead of 100 requests is close to free quota.
Queue and pace. For a burst of work, like syncing 10,000 records, don't fire it all at once. Queue it and drain it at a steady rate below the limit. Job queues such as BullMQ have rate limiting built in.
Identify yourself. A descriptive User-Agent with contact details is mandatory on the National Weather Service API and earns 20 times the quota on Wikimedia. It costs one header.
backoff, its API sits out exactly that long while its public feed fills in.
When the free tier isn't enough
Pay for a higher tier. If your product depends on an API, paying for it is usually the right call.
Cache harder. Refresh less often, serve stale data when fresh data isn't available, and move fetching out of the request path into a scheduled job.
Switch providers or self-host. Bitcoin's price is published by Coinbase, Kraken, Binance (outside the US), CoinGecko, CoinLore and dozens more, so if one limit is too tight, rotate. Some data can be self-hosted outright: mempool.space and the Frankfurter currency API are both open source.
See these patterns running live: the TerminalFeed dashboard keeps 100+ panels fed from rate-limited sources, and our 429 reference covers the status code in more depth.
Rate limits aren't a problem to fight. They're a design parameter. Read the headers, honor Retry-After, cache aggressively and randomize your retries, and the wall you hit on day one mostly disappears.