Implement Production Rate Limiting in 90 Minutes for Operators

The production default for most APIs is a Sliding Window Counter backed by Redis, because it delivers near-exact counting at O(1) memory cost, but bursty developer-facing APIs often do better with a Token Bucket. Either way, you need a clear counting dimension, a centralized fast store with atomic check and update logic, standard rate limit headers, and clients that back off with jitter instead of hammering you on rejection. The algorithm and implementation sections below walk through why.
TL;DR:
- Most APIs default to Redis-backed sliding window counters for near-exact, low-memory rate limiting suited for high-scale environments.
- Combining multiple dimensions like IP, API key, or user ID enhances fairness and effectiveness against abuse.
- Centralized, atomic enforcement via Redis Lua scripts prevents race conditions and ensures consistent across distributed systems.
- Use
429responses withRetry-Afterheaders and clear documentation to improve client recovery and transparency.- Testing with real traffic shapes and fail-open fallback strategies are crucial before deploying rate limits at scale.
Table of Contents
- What Rate Limiting Actually Means in Practice
- Why Rate Limiting Matters More Than Most Teams Assume
- How Do Token Bucket, Leaky Bucket, and Sliding Window Compare?
- Building Rate Limit Enforcement That Survives Distributed Traffic
- Rate Limit Best Practices for Headers, Tiers, and Client Retries
- How Do You Test Rate Limits Before They Hit Production Traffic?
- Why Simple Login Lockouts Fail and What to Use Instead
- Handling GraphQL, Complex Queries, and Shaping Versus Policing
- Handling Distributed Rate Limiting in Microservices
- Strategies for Rate Limiting in Serverless Architectures
- Legal and Compliance Considerations for Rate Limiting
- How Rate Limiting Affects User Experience
- Hard Limits vs. Soft Limits: When to Use Each
- What We’ve Learned Building and Shipping Rate Limits
- Sources
- FAQ
What Rate Limiting Actually Means in Practice
Rate limiting caps how many requests a client can make in a given time window. Throttling is the mechanism that delays or rejects requests once that cap is hit. A quota is the broader allowance, often daily or monthly, while a burst allowance lets a client exceed the steady rate briefly without penalty. These terms get used interchangeably in casual conversation, but they describe distinct control points in a system, and mixing them up in your API docs confuses the developers consuming your service.
When a client crosses the limit, the standard response is HTTP 429 Too Many Requests, paired with a Retry-After header telling the client how long to wait before trying again. That combination is the industry norm, and skipping it is one of the more common mistakes teams make when they bolt rate limiting onto an existing API as an afterthought.
Header conventions have converged around a few patterns. Many APIs still use the informal X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers, which work but were never formally standardized. An IETF draft for structured RateLimit and RateLimit-Policy headers has been circulating for years to fix that inconsistency, and adopting it now positions your API ahead of the eventual standardization curve. Postman’s rate limiting guide walks through both header conventions and the 429/Retry-After semantics clients expect.
Before you write a single line of limiter code, you need to decide what dimension you’re counting against. The choice changes everything downstream, from data structure to fairness:
- IP address: simple, but easily defeated by rotating proxies or shared NAT gateways.
- API key: the most common dimension for authenticated APIs, tying limits directly to a billing or usage tier.
- User ID: useful when one API key serves multiple end users, like a multi-tenant SaaS token.
- Endpoint or path: lets you set stricter limits on expensive operations (search, export, AI inference) than on cheap ones (health checks).
- Tenant: for B2B platforms, limiting by organization rather than by individual user prevents one customer’s traffic spike from starving another.
Most production systems combine two or three of these, layering an IP-based limit for anonymous abuse protection on top of a per-API-key limit for legitimate usage tiers.
Why Rate Limiting Matters More Than Most Teams Assume
Rate limiting exists to protect three things: your infrastructure, your bill, and your users’ fair share of a shared resource. Skip it, and any one of those three can fail without warning.
Downstream systems are usually the weakest link. Your database might handle 10,000 queries per second, but the third-party payment processor or LLM provider you’re calling for every request might cap out far lower, and their rate limit becomes your outage. Teams building on metered APIs (OpenAI, Stripe, Twilio) routinely discover this the hard way when a single misbehaving client script racks up thousands of calls in minutes and the vendor throttles the whole account, not just that one client.
Cloudflare’s own infrastructure absorbs some of the largest recorded traffic floods on the internet, and rate limiting at the edge is one of the core mechanisms it uses to keep origin servers from collapsing under attack traffic. That scale is why its published counting benchmarks carry real weight for engineers picking an algorithm.
Rate limiting also protects against a specific set of attack patterns that firewalls alone don’t catch:
- Distributed denial-of-service traffic that overwhelms compute before it ever reaches your application logic.
- Scraping bots that quietly copy your catalog or content at a rate designed to stay under casual detection.
- Credential stuffing, where attackers replay leaked username and password pairs against your login endpoint at scale.
- A single runaway client (a buggy retry loop, an unthrottled cron job) that accidentally DoSes your own API.
There’s a business upside too. A well-designed tiered limit structure doubles as a monetization lever: hitting a quota is the moment a free user considers upgrading, and a clear, well-worded 429 response with an upgrade link converts better than a silent failure ever will.
How Do Token Bucket, Leaky Bucket, and Sliding Window Compare?
Four algorithms cover the vast majority of production rate limiting: fixed window, sliding window (log or counter), token bucket, and leaky bucket. Each makes a different tradeoff between memory cost, accuracy, and how it handles bursts, and picking the wrong one for your traffic pattern is a common source of either angry users or blown budgets.
Fixed window is the simplest: count requests in discrete time blocks (say, 0:00 to 0:59), reset the counter every minute. It’s cheap, using a single counter per client per window, but it has a well-known flaw at window boundaries. A client can send its full quota at 0:59 and again at 1:00, doubling its effective rate in a two-second span. That boundary burst problem is why fixed window rarely survives contact with real adversarial traffic, though it’s still fine for low-stakes internal limits where nobody is trying to game the edges.
Sliding window log fixes the boundary problem by storing a timestamp for every request and counting how many fall within the trailing window. It’s exact, but memory cost scales with request volume, which gets expensive fast at high throughput. It fits best when you have a small number of clients making relatively few requests each, and exactness matters more than storage efficiency.
Sliding window counter approximates the sliding log using two fixed-window counters and a weighted estimate based on elapsed time into the current window. Cloudflare’s own production analysis found this approach produces a 0.003% error rate across 400 million requests, while keeping memory cost at O(1) per client. That combination of near-perfect accuracy and constant memory is why it’s the default recommendation for public APIs at scale.
Token bucket models a bucket that refills at a fixed rate and drains one token per request. Clients can burst up to the bucket’s capacity, then must wait for tokens to regenerate. This makes it the natural fit for developer-facing APIs where legitimate clients sometimes need to fire off a batch of calls at once, like a CI pipeline uploading build artifacts. Redis’s own use-case documentation describes token bucket as one of the more common patterns implemented with its native data structures.
Leaky bucket is the inverse mindset: requests queue up and drain at a fixed rate, smoothing traffic rather than allowing bursts. It’s the right call when you need a strictly even output rate, such as feeding a downstream system that can’t tolerate spikes at all, but it adds queueing latency that token bucket and sliding window don’t.
| Algorithm | Memory per client | Accuracy | Burst tolerance | Redis implementation |
|---|---|---|---|---|
| Fixed window | O(1) | Low (boundary bursts) | Poor | Single INCR + EXPIRE |
| Sliding window log | O(n) requests | Exact | Good | Sorted set (ZADD/ZREMRANGEBYSCORE) |
| Sliding window counter | O(1) | 0.003% error rate (Cloudflare data) | Good | Two counters + Lua weighting |
| Token bucket | O(1) | High | Excellent (by design) | Lua script updating tokens + timestamp |
| Leaky bucket | O(1) or queue depth | High | None (by design) | Lua script or external queue |
For most public APIs, sliding window counter is the right starting point: constant memory, near-exact enforcement, and no boundary exploit. Reach for token bucket specifically when your traffic pattern includes legitimate bursts you want to accommodate rather than punish, like webhook delivery retries or batch uploads. Leaky bucket earns its place when you’re rate limiting output toward a fragile downstream consumer rather than gating inbound abuse. Sliding window log is worth the extra memory only when you have few enough clients that exact accuracy doesn’t strain your store, and a compliance or billing requirement demands precision.
Pro Tip: Don’t pick an algorithm in the abstract. Pull a week of real traffic logs and simulate all four against it. The boundary-burst weakness in fixed window only shows up under specific traffic shapes, and you’ll often find it’s a non-issue for your actual usage pattern, which saves you from over-engineering a sliding window counter you didn’t need.
Building Rate Limit Enforcement That Survives Distributed Traffic
A rate limiter that only works on one server doesn’t work at all once you scale past a single process. If each application instance keeps its own in-memory counter, a client can get three times the intended limit just by having requests routed across three servers. That’s why centralized, fast, atomic state is the core engineering problem here, not the algorithm choice itself.
Redis has become the default answer for this because it gives you exactly the primitives a rate limiter needs. INCR and EXPIRE handle basic fixed-window counting in two commands. Sorted sets (ZADD, ZREMRANGEBYSCORE) support sliding window log implementations by storing timestamped entries you can prune and count. And Lua scripting lets you execute a full check-and-update sequence as a single atomic operation on the Redis server itself, which matters more than it sounds.
The read-then-write gap in a naive rate limiter is where most implementations quietly break. You check the counter, decide the request is allowed, then increment it. Between those two steps, another request on another thread can slip through the same gap, and under load that race condition compounds into limits that leak far more capacity than intended.
That’s a textbook time-of-check-to-time-of-use race, and it’s exactly what Lua scripts in Redis solve: the entire read, evaluate, and write sequence runs as one atomic unit on the server, with no window for a second request to interleave. Redis’s documentation explicitly recommends this pattern over client-side MULTI/EXEC transactions, which don’t protect against the check happening before the transaction even opens.
A few implementation choices worth locking in early:
- Push enforcement to edge infrastructure (Cloudflare, a CDN, an API gateway) when you can, since rejecting abusive traffic before it reaches your origin saves compute and bandwidth you’d otherwise pay for.
- Keep origin-level limits as a second layer even with edge enforcement in place, since not every client route passes through the same edge path.
- Use per-process limits (like NGINX’s
limit_reqmodule) only as a coarse, cheap first line of defense, since they can’t see traffic hitting sibling instances. - Store rate limit state with a short TTL matching your window size so Redis memory doesn’t grow unbounded across millions of distinct clients.
- For global services spanning regions, either accept a bounded inaccuracy with per-region caps reconciled at the origin, or invest in active-active replication if you need tighter global consistency.
The hardest decision isn’t algorithmic. It’s what happens when your rate limiting store goes down. The instinct to fail closed, blocking all traffic until Redis recovers, feels safer, but it turns an infrastructure blip into a total outage. The more defensible default is failing open: if the limiter can’t reach its store within a tight timeout, let the request through and log the miss, while a circuit breaker and degraded-quota fallback catch sustained failures before they become an abuse vector.
Rate Limit Best Practices for Headers, Tiers, and Client Retries
Getting the algorithm right solves half the problem. The other half is making the limit predictable and recoverable from the client’s point of view, which is where a lot of technically correct implementations still frustrate the developers using them.
- Standardize your headers. Adopt the IETF draft’s
RateLimitandRateLimit-Policyheaders where your framework supports them, and fall back to the widely understoodX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resettrio otherwise. Always includeRetry-Afteron a 429 response, since it’s the one header most HTTP clients and libraries already know how to read automatically. - Document limits per tier, publicly. A developer should be able to find your rate limits in your docs without opening a support ticket. Pair that documentation with a visible upgrade path, so hitting a limit reads as an invitation rather than a dead end. Teams issuing high-volume email sends run into the same tiering logic; guidance on Gmail’s sending caps is a useful reference for how a well-documented tier structure maps server capacity to client-visible quotas.
- Write useful error bodies. A 429 response body that states the limit, the current usage, and the reset time saves the client a support ticket and saves you the ticket triage.
- Monitor limit hits as a first-class metric. Track your 429 rate over time, identify your top offenders by client ID, and separately flag false positives (legitimate clients getting throttled) versus genuine abuse.
- Alert on unexpected spikes, both in limit hits and in requests approaching the limit, since a sudden jump often signals either an attack starting or a client’s retry logic misbehaving.
- Guide clients toward exponential backoff with full jitter. AWS’s widely cited backoff research found that adding randomized jitter to exponential backoff substantially reduces the contention that happens when many clients retry in lockstep after a shared failure. Publish a recommended backoff snippet in your docs rather than assuming every SDK author will implement it correctly on their own.
- Decide between queueing and immediate rejection. For non-interactive workloads (batch jobs, webhook deliveries), consider a queue that smooths bursts instead of a hard reject, but keep interactive, user-facing endpoints on immediate 429 responses so the UI can react in real time.
Pro Tip: Log every rate limit rejection with the client’s actual usage pattern in the seconds leading up to it. Most “false positive” complaints turn out to be legitimate clients whose retry logic has no backoff at all, hammering the same endpoint every 100 milliseconds until the limit trips. Fixing their retry logic, not your limit, is usually the real answer.
How Do You Test Rate Limits Before They Hit Production Traffic?
A rate limiter you haven’t load tested is a guess dressed up as a policy. Verify it against three distinct traffic shapes before trusting it with real users: a burst right at a window boundary (to catch the fixed-window edge case), a sustained high-throughput run at your target ceiling, and a distributed test firing from multiple nodes simultaneously to confirm your centralized store actually enforces the limit consistently across instances.
Postman’s testing guidance recommends using its runner feature to script exactly this kind of repeated-burst scenario against a live endpoint, which is a low-friction way to validate limiter behavior without standing up a separate load testing framework. Pair that with a heavier tool for the sustained-throughput runs, since Postman’s runner isn’t built for generating thousands of requests per second on its own.
While testing, capture these metrics specifically:
- Your 429 rate as a percentage of total requests, both overall and broken down per client.
- Per-client counters over time, to confirm the algorithm is tracking each identity dimension correctly rather than leaking state across clients.
- The distribution of
Retry-Aftervalues you’re returning, since a limiter that always returns the same value regardless of actual remaining wait time isn’t calculating correctly. - The latency your rate limit check itself adds to each request, since a poorly optimized Lua script or an unindexed sorted set can add meaningful overhead at scale.
Run a chaos test where you deliberately kill the Redis connection mid traffic run, and confirm your fail-open behavior actually engages rather than throwing unhandled errors. Then validate the recovery path once the store comes back, checking that counters don’t reset in a way that gives every client a fresh burst allowance the instant Redis reconnects. Roll new limiter logic out through a canary deployment against a small percentage of production traffic first, watching the same metrics before flipping it on for everyone.
Why Simple Login Lockouts Fail and What to Use Instead
Locking an account after five failed login attempts feels like obvious security. It’s also a documented attack vector: an adversary who knows a victim’s username can lock them out of their own account just by intentionally failing the password five times, turning your defense into a denial-of-service tool aimed at your own users. OWASP’s guidance on brute-force protection is direct about this limitation and recommends layering in additional signals rather than relying on lockouts alone.
A stronger login rate limiting setup for brute force protection combines several lighter-touch signals instead of one blunt trigger:
- Progressive delays that add a growing wait after each failed attempt, rather than a hard lockout, so a genuine user who mistypes their password once isn’t fully shut out.
- Device cookies or fingerprints that let you distinguish a user’s usual device from an unfamiliar one, tightening limits only on the unrecognized case.
- Per-IP throttles layered alongside per-account limits, since credential-stuffing attacks typically hit many accounts from a shared pool of IPs.
- CAPTCHA challenges triggered after a handful of failures, adding friction that’s cheap for a human but expensive for automated attempts at scale.
- Reputation signals from IP or device history, letting you apply stricter limits to sources already flagged for abuse elsewhere on your platform.
Count only failed responses toward your rate limit, not every request to the login endpoint. A user correctly entering their password on the first try shouldn’t consume the same budget as a bot spraying guesses, and response-based counting keeps your limit focused on actual abuse signal rather than raw traffic volume. Whitelist your own health checks and monitoring probes explicitly, since nothing looks more like an internal false alarm than your uptime monitor tripping its own login endpoint’s rate limit every five minutes.
Handling GraphQL, Complex Queries, and Shaping Versus Policing
GraphQL breaks the simple “count requests per endpoint” model, because a single POST to /graphql can request one field or fifty nested ones, and a request-count limit treats both identically while one costs a hundred times more compute. The fix is a cost or complexity budget: assign each field or resolver a point value, sum the total for an incoming query, and reject or throttle anything that exceeds the client’s allotted budget per window. Depth limits work as a simpler complement, capping how many levels of nested relationships a single query can traverse.
Composite keys extend the same idea to REST. Instead of limiting by API key alone, key your counter on a combination like path plus user ID plus a specific query parameter, which lets you apply a stricter limit to an expensive /export endpoint than to a cheap /status check, even for the same authenticated user.
The last decision is whether to shape traffic or police it. Policing means rejecting anything over the limit outright with a 429, which is the right call for interactive, latency-sensitive endpoints where a delayed response is as bad as a rejected one. Shaping means delaying or queueing excess requests instead of rejecting them, smoothing a burst into a steady trickle the backend can absorb. Cloud provider rate limiting documentation draws this same distinction between throttle and ban actions, and the choice usually comes down to whether the client can tolerate a delayed response or needs an immediate answer either way.
Handling Distributed Rate Limiting in Microservices
Once you split a monolith into microservices, “the API” isn’t one enforcement point anymore. It’s a mesh of internal calls, any of which can amplify a single external request into dozens of downstream ones. A naive per-service limiter checks its own traffic in isolation and misses the fact that five internal services are all hammering the same shared database because of one upstream spike.
The practical fix is centralizing rate limit state in a shared, fast store, typically Redis, that every service checks against rather than maintaining its own isolated counters. This is the same atomicity problem covered earlier, just multiplied across service boundaries: every microservice needs to run the same Lua-based check-and-update logic against the same keys, or you’re back to the race conditions a single-process limiter already solved.
A service mesh or API gateway layer (Envoy, Kong, or a cloud provider’s managed gateway) is often the cleanest place to enforce limits consistently, since it sits in front of every service call and can apply the same policy without each individual service reimplementing limiter logic. Where you can’t route everything through a shared gateway, propagate a consistent client identity (a trace ID, tenant ID, or user ID) through internal headers so every service that does check limits is counting against the same identity dimension.
Watch specifically for cascading amplification: one external request that fans out into calls across five internal services turns your external limit into a false sense of safety, because internal call volume can spike far higher than the external rate implies. Rate limit at internal service boundaries too, not just at the public edge, particularly for services that sit behind expensive or metered dependencies.
Strategies for Rate Limiting in Serverless Architectures
Serverless functions complicate rate limiting because there’s no long-running process to hold in-memory state, and every invocation potentially runs on a fresh execution context with no memory of what came before it. That rules out any limiter design that assumes a persistent local counter, which pushes you toward the same conclusion as the microservices case: state has to live in an external, shared store that every invocation can reach quickly.
Latency becomes a sharper constraint here than in a traditional server setup, because a slow rate limit check adds directly to your function’s billed execution time. A managed Redis instance or a cloud provider’s edge key-value store, kept in the same region as your functions, keeps that added latency in the low single-digit milliseconds rather than compounding your cold-start penalty with a slow round trip to a distant data store.
Concurrency limits deserve separate attention from request-rate limits in serverless environments, since most providers cap how many function instances can run simultaneously regardless of your rate limiting logic. A rate limiter that permits a burst your platform’s concurrency ceiling can’t actually execute will just convert 429s into cold-start queuing delays or outright throttling errors from the platform itself, so check your provider’s concurrency limits before you set your own rate thresholds.
Where your serverless platform sits behind an API gateway, push as much enforcement to that gateway layer as you can. It’s usually cheaper (no billed function time spent evaluating a limit) and catches abusive traffic before it triggers a cold start at all, which matters more for cost control in serverless billing than it does in a traditional always-on server.
Legal and Compliance Considerations for Rate Limiting
Rate limiting itself isn’t a regulated activity, but the data you collect to enforce it can be. Tracking IP addresses, device fingerprints, or user identifiers to build rate limit counters means you’re processing personal data under frameworks like the EU’s GDPR, even when the sole purpose is security rather than marketing or profiling.
The practical implication is retention, not prohibition. Rate limit counters typically need to persist only for the duration of the active window, often seconds to minutes, so the safest approach is letting that data expire automatically (a Redis TTL matching your window handles this by default) rather than accumulating a long-term log of every client’s request history without a clear retention policy. If you do retain rate limit event logs longer for abuse investigation, document that retention period and its security justification the same way you’d document any other personal data processing activity.
Cross-border data transfer rules can apply if your centralized rate limiting store lives in a different region than the users generating the traffic, which matters more for global services running a single shared Redis instance than for regionally partitioned deployments. This isn’t a substitute for legal advice specific to your jurisdiction and user base, but it’s worth flagging to whoever owns compliance at your company before you assume rate limiting sits entirely outside data protection scope.
How Rate Limiting Affects User Experience
A rate limit is invisible right up until it isn’t, and the moment it triggers is the moment your API’s error handling quality becomes very visible to the developer or user on the other end. A bare 429 with no explanation reads as broken. A 429 with a clear Retry-After header, a documented limit, and an error body explaining what happened reads as a system that knows what it’s doing.
Graceful degradation is the difference between those two experiences. Rather than a hard cutoff, consider serving a reduced feature set once a client approaches its limit, cached or slightly stale data instead of a live query, lower-resolution results instead of a full response, or a queued response instead of an immediate rejection for non-interactive requests. Each of these keeps the client functional at reduced capacity rather than fully blocked.
Warning users before they hit a wall matters as much as how you handle the wall itself. Surfacing remaining quota in a dashboard or via response headers lets a developer building against your API self-regulate before they ever see a 429, which cuts support tickets and frustration in roughly equal measure. For consumer-facing products, a visible “you’re approaching your limit” notice does the same job in plain language.
The tone of your rate limit messaging matters more than most teams expect. An error body that reads as punitive (“You have been blocked”) drives frustration and support escalations, while one that reads as informative (“You’ve used 950 of 1,000 requests this hour, resets at 3:15 PM, upgrade for higher limits here”) turns the same technical event into a far less contentious interaction.
Hard Limits vs. Soft Limits: When to Use Each
A hard limit rejects every request over the threshold, no exceptions, full stop. A soft limit allows some flexibility, whether that’s a burst allowance above the stated cap, a warning period before enforcement kicks in, or a degraded response instead of an outright rejection. The difference isn’t about the number, it’s about what happens the instant a client crosses it.
Hard limits fit anywhere the cost of an over-limit request is severe: a metered third-party API where every extra call adds directly to your bill, a security-sensitive endpoint like login where excess attempts represent genuine risk, or infrastructure genuinely at its breaking point where “just a few more requests” isn’t actually possible. The tradeoff is unforgiving to legitimate clients who have a one-time legitimate reason to briefly exceed their normal pattern.
Soft limits fit better for consumer-facing features and most internal or B2B usage tiers, where a temporary burst above the stated cap costs you very little but rejecting it outright costs you goodwill. A soft limit implementation might use a token bucket’s natural burst tolerance, or a policy that logs and warns on the first overage before enforcing on a second. The tradeoff runs the other way: implemented carelessly, soft limits give a determined abuser room to operate just below wherever enforcement actually kicks in.
Most mature APIs run both at once, layered by severity. A soft, generous limit handles normal usage variance without friction, while a hard limit sits further out as a backstop against genuine abuse or infrastructure protection. Treat the soft limit as the one your typical user should never notice, and the hard limit as the one that exists purely to keep the system alive.
What We’ve Learned Building and Shipping Rate Limits
The biggest mistake teams make isn’t picking the wrong algorithm. It’s shipping limits that are too tight, too early, based on guessed traffic patterns instead of observed ones. A public beta with aggressive limits doesn’t protect you, it just makes your product feel broken to the first cohort of users you’re trying to win over. Start generous, instrument everything, and tighten only where the data tells you to.
Fail-open deserves more respect than most teams give it. Blocking all traffic because your rate limit store had a two-second blip is a self-inflicted outage over a problem that didn’t need to become one. The engineering discipline is in the fallback: circuit breakers, degraded quotas, and clear logging so a fail-open event doesn’t quietly turn into an unmonitored abuse window.
If you’re starting from zero today, here’s a sprint you can realistically finish in about ninety minutes: stand up a basic sliding window counter in Redis using two keys and a Lua script for the weighted estimate, wire your API to return 429 with a Retry-After header on rejection, write one paragraph of docs stating the actual limit per tier, and add a single dashboard metric tracking your 429 rate. That’s not a finished system, but it’s a real one, and it’s far ahead of shipping no limit at all while you plan the perfect version.
This is exactly the kind of production-readiness gap Vibeprod exists to catch. A rate limiter with no fail-open path, an auth endpoint with lockout logic that enables its own DoS, a Redis connection string committed in plaintext, these are the launch risks that slip through when you’re moving fast on core features. Vibeprod scans your GitHub repository, flags exactly this kind of gap in plain English, and opens a reviewable pull request to fix it, without touching the features you already shipped, so you can get back to the logic that actually differentiates your product.
— Vibeprod
Sources
Bookmark these before you start writing limiter code. Redis’s rate limiter documentation covers the data structures and Lua patterns for atomic enforcement. Cloudflare’s engineering blog breaks down the sliding window counter’s production accuracy at scale. Postman’s API rate limiting guide covers header conventions and practical testing. OWASP’s brute-force controls page explains why lockouts alone fail. Digital Applied’s engineering reference rounds out the algorithm comparison with backoff and atomicity guidance.
- Counting things: a lot of different things — Cloudflare blog
- What is API rate limiting? — Postman blog
- Rate limiter — Redis use cases
- Blocking brute force attacks — OWASP
FAQ
What Are the Different Types of Rate Limiting?
The main types are fixed window, sliding window (log or counter), token bucket, and leaky bucket, each trading off memory cost, accuracy, and burst tolerance differently. Most production systems also layer limits by dimension, combining IP-based, API-key-based, and endpoint-specific limits rather than relying on a single type.
What Are the Common Approaches to Rate Limiting?
Beyond algorithm choice, the main approaches are enforcing at the edge (a CDN or gateway) versus at the origin, using centralized stores like Redis for distributed consistency, and choosing between hard rejection (policing) and delayed processing (shaping). Auth endpoints typically add a separate layer combining rate limits with progressive delays and CAPTCHA.
What Is the Best Algorithm for Rate Limiting?
For most public APIs, the Sliding Window Counter is the strongest default, since Cloudflare’s production data shows a 0.003% error rate across 400 million requests at O(1) memory cost per client. Token bucket is usually the better choice when your API needs to tolerate legitimate traffic bursts.
What Is an Example of a Rate Limiter?
A typical implementation uses Redis to store two fixed-window counters per client, with a Lua script that atomically reads both, calculates a weighted sliding-window estimate, and either allows the request or returns a 429 with a Retry-After header. That pattern, documented in Redis’s own use-case guide, covers the majority of production API rate limiting needs without custom infrastructure.