429 Too Many Requests: Rate Limiting from Both Sides
You are usually on both ends of this at once — limited by somebody's API and limiting your own. One header joins the two sides, and almost nobody sends or reads it properly.
Search for this status code and Google will tell you to clear your browser cache, disable your extensions, deactivate WordPress plugins one at a time, and switch networks or use a VPN to change your IP address. That is genuinely the advice in the generated answer at the top of the results as of writing, and for someone who cannot load a shopping site it is the right advice.
It is not the advice you need if you write the software. In the eleven items on that first page, exactly one is aimed at a developer: a Stack Overflow question from twelve years ago, from which Google highlights four words that are the best summary of the whole topic.
This is written for the developer end of it, and specifically for the situation almost everyone is actually in — being rate limited by an API you depend on while rate limiting the API you operate. They are the same problem seen from two ends, and one HTTP header joins them.
429 is not an error
That is the phrase Google pulls out of that Stack Overflow answer, and it is worth taking literally. The answer continues: "1) Sleep your process. 2) Exponential backoff. If the server does not tell you how long to wait, you can retry...".
A 429 is the server successfully telling you something. Nothing failed: your request was understood, it was well-formed, you were authenticated, and the answer is that you are going faster than the agreement allows. Compare it with a 500, which means something broke and retrying might work, or a 403, which means stop and do not come back. A 429 means come back later, and it usually says how much later.
The distinction matters because of what a client does next. Code that treats 429 as a failure typically does one of two things, and both make it worse. It retries immediately, adding load to a server that just asked for less. Or it raises an exception into a queue of pending work, so the retry storm arrives all at once when someone restarts the process. Treating it as a control signal instead means reading what the signal says.
Retry-After, and what to do when it is missing
The header exists for exactly this. A well-behaved limiter answers like this:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
{"error":"rate_limit_exceeded","detail":"100 requests per minute per API key"}Retry-After takes two forms and clients need to handle both. It is either a number of seconds to wait, as above, or an HTTP-date giving the moment the limit lifts. The date form is the one that breaks naive parsers, and it is the one a limiter that resets on a fixed clock boundary will usually send.
The RateLimit-* family is the newer half of that response and the useful part for a well-written client, because it can slow down before hitting the wall rather than after. These headers are the subject of an IETF draft as of writing, so the exact names are worth checking against your dependencies; the X-RateLimit-* spellings remain widespread as the de facto predecessor and plenty of APIs still send those instead.
Exponential backoff with jitter, and why the jitter is the important half
When there is no Retry-After, you back off. Doubling the wait after each failure is the well-known part and it is not sufficient on its own, because every client that started at the same time doubles on the same schedule and they all come back together. The randomness is what breaks up the convoy.
# Wait for what the server asked for; back off with jitter when it did not say.
delay = response.headers.get("Retry-After")
if delay is not None:
wait_seconds = parse_retry_after(delay) # seconds, or an HTTP-date
else:
base = min(MAX_BACKOFF, INITIAL_BACKOFF * 2 ** attempt)
wait_seconds = random.uniform(0, base) # full jitter, not base + jitterTwo details that get skipped. A cap on the backoff keeps a long outage from producing a retry scheduled an hour out, which is usually worse than failing cleanly. And a limit on attempts matters more than the delay curve: work that will never succeed should end up somewhere a human can see rather than being retried until the process restarts.
When you are the one being limited
Look at what people actually search alongside this status code and it is a directory of other people's products: Shopify, HubSpot, GitLab, Steam, Docker, Spotify, DeepL, Okta, Etsy, npm, Jira, Discord, Deutsche Bahn. The 429 you are reading is nearly always somebody else's decision about you, delivered after the fact.
That shapes the response. You cannot negotiate with it in code, so the work is to (a) find the documented limit before you meet it, (b) make the limit visible in your own monitoring rather than discovering it in a support ticket, and (c) decide which calls are allowed to fail and which have to be retried later, which is a queue question rather than a client question.
429 Too Many Requests OpenAI
That is a real related search on this results page, sitting next to 429 too many requests chatgpt and 429 too many requests gemini. Model APIs are now one of the most common sources of this status code, and they behave differently enough from a normal REST quota to be worth separating.
As of writing, these limits are typically two-dimensional: a cap on requests per minute and a separate cap on tokens per minute, and you can be nowhere near the first while sitting on the second. A long prompt consumes quota that a short one does not, so the same number of calls can pass in the morning and fail in the afternoon because the inputs got bigger. Retrying a second later achieves nothing when the window is a minute wide and your own traffic is what filled it.
The structural answer is that model calls belong behind a queue rather than in a request handler. The work is slow, the quota is shared, and the failure is temporary, which is the exact profile the worker and queue shape exists for. A user waiting on a synchronous call gets an error; a job in a queue gets retried in a minute and nobody notices.
When you are the one limiting
The other end. Three algorithms cover almost everything, and they differ mainly in what they do at the boundary of the window.
| Algorithm | What it stores | Behaviour at the boundary | When to pick it |
|---|---|---|---|
| Fixed window | One counter per key per window | Allows a double burst: the full quota at the end of one window and again at the start of the next | Cheap, simple, fine when the limit is a courtesy rather than a protection |
| Sliding window | A counter per sub-window, or timestamps | Smooth, no double burst, at the cost of more memory and more work per request | Public APIs where the boundary burst is the abuse case |
| Token bucket / GCRA | A token count and a last-refill timestamp | Allows a deliberate burst up to bucket size, then enforces a steady rate | Interactive traffic, where a short burst is normal user behaviour |
go-redis/redis_rate package describes itself as implementing "GCRA (aka leaky bucket)". The bucket size is the part worth thinking about, because it is the burst you are explicitly allowing.Whichever you pick, send the headers from the first section. A limiter that returns 429 with no Retry-After is asking every client to guess, and the guesses will be wrong in the direction that costs you: too short, and the retry storm continues; too long, and legitimate users are locked out of a limit that lifted a minute ago.
Don't use Redis as a rate limiter
That is the title of a Hacker News thread from August 2025 which currently sits in the top ten for redis rate limiting, three positions below Redis's own documentation on how to do it. A Medium series called "Why you shouldn't use Redis as a rate limiter" ranks between them. The official guidance and the argument against it are on the same page of results, and nobody has written the piece that resolves it.
The strongest version of the objection
From the Hacker News thread, verbatim: "Rate-Limiting is always a deployment specific feature. If it's for limiting user requests, then it should be a component of the ingress/API...". The point is that a limiter which only triggers after a request has reached your application has already cost you the connection, the routing and a round trip to Redis, which is the wrong place to absorb an attack.
That is correct, and it is an argument about one layer rather than about Redis. Rate limiting happens at more than one level, and the levels want different tools.
| Level | What it can see | What it is good at | What it cannot do |
|---|---|---|---|
| CDN or edge | IP, path, headers | Absorbing floods before they cost you anything | Knowing who the user is, or what their plan entitles them to |
| Ingress or reverse proxy | IP, path, sometimes an API key | Cheap coarse limits close to the door | Per-tenant quotas that depend on your database |
| Application | The authenticated user, the tenant, the plan, the cost of the call | Exactly the limits you sell and the ones you owe an upstream | Stopping traffic before it arrives |
Redis belongs to the third row, and the reason is narrow: the counter has to be shared. Your application runs as more than one instance, and a per-process counter gives each instance its own allowance, so a limit of 100 per minute silently becomes 100 times however many instances you happen to be running. An in-memory counter is not a rate limiter for anything horizontally scaled; it is a per-instance suggestion.
The counter is durable data in a keyspace you treat as a cache
This is the failure worth planning for and it is quiet. Rate-limit counters usually live in the same Redis instance as the cache, because that is the Redis that already exists. They are not cache: a cache entry can be recomputed and a counter cannot. If that instance is configured with an allkeys-lru policy and memory gets tight, the counters are eligible for eviction along with everything else, and evicted counters mean the limit stops applying without a single error anywhere.
The general form of that trap, along with what the eight eviction policies actually do and why a mixed instance has no correct one, is in the write-up of the OOM error this produces at the other extreme. The short version for a limiter: counters want a TTL and an instance whose policy will not delete them, and deciding what belongs in Redis at all is the version of this question asked early.
Your rate limiter keeps an access log
A rate limiter works by remembering who did what and when. The keys look innocuous, and they are records about people.
ratelimit:ip:203.0.113.45:2026-08-28T09:14 -> 63
ratelimit:user:8241:login -> 4An IP address is personal data under the GDPR, and a per-IP limiter is therefore a log of access times tied to an identifier, sitting in a datastore that usually has no retention policy because nobody thought of it as storage. Login attempt counters are the same thing with a sharper edge, since they record failed authentication against a named account.
Two things follow, and neither is expensive. Give every limiter key an explicit TTL, sized to the window rather than left to whatever the instance does under pressure, so the data disappears because you decided it should rather than because memory ran out. And keep the instance in a jurisdiction you can account for: where the data physically rests and who is a processor for it applies to a counter exactly as it applies to a database row, and the counter is easier to forget.
A note for anyone who arrived here from biochemistry
"Rate-limiting step" is a standard term in enzyme kinetics, and on that exact phrase the biochemistry sense outranks the software one: the neighbouring searches are for the rate-limiting enzyme of glycogenolysis and the rate-limiting step of cholesterol synthesis. If that is what you were looking for, nothing on this page will help. The two fields borrowed the same words for the same underlying idea, which is the slowest stage setting the pace of everything downstream.
How Runsite handles it
The precise, per-user half of the table above is application code, and it needs a counter that every instance of that application shares. Managed Redis hosted in the EU is where that counter goes, on the same invoice as the application itself, which means one network hop rather than a round trip to another provider on every limited request.
To be clear about what we do not do: there is no built-in limiter at the edge, so the coarse layer is yours to place. What the platform removes is the operational part — an instance with a memory ceiling set by the plan rather than left at a default, so the eviction problem above does not arrive by accident. Servers are in Germany as of writing and a signed GDPR data processing agreement covers every plan including the free one, which is the piece the previous section is about: those counters are personal data and they are sitting somewhere specific.
Being fair about the trade: none of the algorithms in this article require a managed anything, and a single-instance application with a counter in process memory is a perfectly good limiter until the day you run two instances. Setup details are in the Runsite docs.
The short version
429 is not an error, in the words Google highlights from the twelve-year-old Stack Overflow answer that is the only developer-facing result on the whole first page. It is the server successfully telling you to slow down.- The first page of results for this status code is written for end users: clear your cache, disable extensions, check your WordPress plugins, change your IP. Correct for them, useless for you.
Retry-Afteris the header that joins both sides, and it takes two forms: a number of seconds or an HTTP-date. Clients need to parse both; the date form is what a limiter resetting on a clock boundary sends.RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetlet a client slow down before it hits the wall. They are an IETF draft as of writing, andX-RateLimit-*remains widespread.- When there is no header, back off exponentially with jitter. The jitter is the half that matters, because synchronised clients doubling on the same schedule return together.
- Cap the backoff and cap the attempts. Work that will never succeed belongs somewhere a human can see it, not in an infinite retry.
- Model APIs are a distinct case. Their quotas are usually two-dimensional — requests per minute and tokens per minute — so the same call count can pass and then fail because the prompts got longer. Those calls belong behind a queue.
- Three algorithms, and the difference is the window boundary: fixed window allows a double burst across it, sliding window does not, token bucket allows a deliberate burst you configured.
- "Don't use Redis as a rate limiter" is right about one layer. Coarse limits belong at the edge where they are cheap; per-user and per-tenant limits belong in the application, which is the only place that knows what the numbers mean.
- The reason the application layer needs Redis is that the counter must be shared. A per-process counter turns a limit of 100 per minute into 100 per instance.
- Limiter counters are not cache. On an instance with an
allkeys-lrupolicy they can be evicted, and an evicted counter means the limit silently stops applying. - A rate limiter is an access log. IP addresses are personal data, login counters record failed authentication, and both want an explicit TTL and a jurisdiction you can account for.