Most rate-limiting tutorials hand you five algorithms and a comparison table, then leave you to guess which one survives contact with production traffic. I've watched enough teams ship the algorithm they read about first and discover the gap the hard way to know the comparison table is the wrong thing to argue about. The algorithm matters less than where you run it and what state it depends on.
The scale numbers make the point for me. During BFCM 2024 alone, our network handled 86.7 billion requests, blocked more than 3 billion firewall events, and held 99.9992% uptime. At that volume, the choice between two algorithms with similar semantics is noise. The choice between a counter you can trust and a counter that lies to you is everything.
Settle three things before you pick an algorithm: which plan tier you're on, where the counter lives, and how stale you're willing to let it get. Get those wrong, and no algorithm saves you.
Copy link to headingKey takeaways
The algorithm you choose matters less than where you enforce it and how trustworthy its counter state is, especially on stateless edge and serverless runtimes.
Token bucket is the strongest default for developer-facing APIs because it tolerates legitimate bursts while capping the sustained rate.
Sliding window counter is the practical pick for high-scale public endpoints, holding fixed memory while staying accurate enough for production.
Edge Config is built for low-latency reads of configuration that changes rarely, not for rate-limit counters that update thousands of times per second.
Rate-limiting meters request counts and never verify whether a caller is human, so bot defense belongs in a separate enforcement layer.
Copy link to headingThe boundary burst fixed window ignores
Fixed window is the algorithm everyone reaches for first, and in my experience, it's also the one teams keep on a public endpoint long after they've outgrown it. The mechanics are honest about what they do:
Time gets divided into fixed windows, with one minute being the common choice.
A counter increments per identity on every request inside the current window.
Requests are rejected until the window resets once the counter hits the limit.
That's the whole algorithm. One counter per identity, almost no memory cost. For a simple internal limit, it's the right amount of machinery and no more.
Copy link to headingWhere it breaks: the boundary
The failure mode lives at the seams. A client can drain the full quota at the tail of one window and drain it again at the head of the next, and both bursts are individually legal. With a 5-requests-per-minute limit:
5 requests at 2:00:59 and 5 more at 2:01:00 can produce 10 requests inside roughly two seconds.
The one-minute span from 2:00:30 to 2:01:30 has now passed 10 requests, twice the rate you thought you enforced.
Scale that up, and a client sends 100 requests in the last second of one window and 100 in the first second of the next: 200 requests in two seconds against a per-minute limit. The counter never sees a violation because it resets before the burst completes.
The algorithm isn't broken. It's doing exactly what it promises, counting within a window and forgetting at the boundary, and the boundary is the attack surface. Put it on a public endpoint, and you've published the exact two-second window an attacker needs to double your rate. That's why I treat a fixed window as disqualified for anything public-facing before the conversation even starts.
Copy link to headingWhere it fits on Vercel
Vercel WAF exposes a fixed window on Hobby and Pro plans, which is the right default for the internal and first-pass limits those tiers tend to carry. Routing Middleware backed by a real counter store is the answer when you need stronger semantics on a public endpoint, which is where the rest of this piece goes.
Copy link to headingSliding window trades memory for accuracy
Sliding window fixes the boundary problem, and it gives you two ways to do it that sit at opposite ends of the cost curve. Understanding which one you're reaching for is the whole decision.
Copy link to headingSliding window log: exact, but it grows
The log-based version stores a timestamp for every request. The system drops timestamps older than the window on each new arrival and counts what remains, which gives you exactly the last N seconds of traffic at any moment. It's the most accurate option available, and the accuracy comes with a bill.
The log creates memory pressure under a traffic spike, plus CPU overhead from pruning old entries, and memory grows with request volume. A per-identity timestamp log at BFCM request volume is not a storage cost I'd sign up for. Sliding window log fits the narrow case where strict fairness is non-negotiable and volume is bounded. You're paying for precision you can't use anywhere else.
Copy link to headingSliding window counter: fixed memory, accurate enough
Sliding window counter is the version most teams should reach for, and it's straightforward to run in Routing Middleware. It keeps two counters, one for the current window and one for the previous, then computes a weighted estimate:
weighted_count = (previous_window_count × overlap_percentage) + current_window_count
Two counters, fixed memory, regardless of how much traffic moves through. The math is worth walking once, because it shows exactly what you're trading. At 15 seconds into the current minute, 45 seconds of the previous minute still overlap the trailing 60-second window. With 42 requests in the previous minute and 18 in the current one, the estimate is: 42 × (45 / 60) + 18 = 49.5 requests.
The estimate assumes traffic was evenly spread across that previous minute, which it rarely is, so the number is an approximation rather than a true count. That's the catch, and it's a catch I'll take every time on a public endpoint.
You give up exactness, and you get bounded memory plus a dramatically smaller attack surface than a fixed window leaves open. The approximation can be wrong near burst boundaries, but it keeps memory bounded while avoiding the fixed-window reset cliff. The memory savings are the difference between a counter that scales and one that falls over at BFCM volume. For scalable public APIs, this is the practical answer.
Copy link to headingToken bucket is the default for developer-facing APIs
I'd hand a developer-facing API token bucket every time, given one algorithm and no context. It treats bursts as a feature instead of a violation, which matches how real clients behave. Each identity gets a bucket with a maximum capacity and a refill rate. Tokens accumulate up to the ceiling; each request spends one, and an empty bucket means rejection.
It's the model behind the throttling you've hit on most large developer APIs, Amazon and Stripe among them, though copying their parameters wholesale is a mistake. The shape transfers. The numbers belong to their traffic, not yours.
Copy link to headingWhy bursts are a feature, not a bug
The behavior that earns the token bucket its default status is burst tolerance. A client who hasn't made requests recently has tokens banked, so a legitimate spike from a flash sale or a batch job goes through instead of getting punished for being bursty.
You tune two parameters, and the tradeoff lies between them:
Capacity sets how large a burst you'll absorb. Too high and you've allowed a burst large enough to hurt the thing you're protecting.
Refill rate sets the sustained ceiling. Set the capacity too low, and you've thrown away the burst tolerance that made the token bucket worth choosing.
Get the two right, and it's a strong general-purpose default.
Copy link to headingWhere it fits on Vercel
On Vercel, the token bucket is the Enterprise-only algorithm in the WAF, and that stratification is deliberate:
Hobby and Pro get a fixed window.
Enterprise gets a fixed window plus token bucket, plan limits that extend counting keys to user agent and arbitrary headers, windows up to an hour, and 1,000 rules per project instead of 40.
You implement token-bucket semantics yourself on Pro if you want them on a public endpoint, and the Upstash SDK supports tokenBucket directly:
const ratelimit = new Ratelimit({ redis: redis, limiter: Ratelimit.tokenBucket(5, "10 s", 10),});Five tokens per ten seconds, bucket capacity ten. Drop that into Routing Middleware backed by Upstash Redis, provisioned through the Vercel Marketplace with vercel install upstash, and you have token-bucket enforcement on any plan even though the WAF reserves it for Enterprise.
Copy link to headingLeaky bucket shapes outbound traffic
Leaky bucket is the one I see misapplied most often, usually by teams that read about token bucket and assumed the two were interchangeable. They aren't, and the difference is the direction of the traffic you're protecting. Picture a FIFO queue that drops requests when full and drains at a fixed outflow rate, no matter how fast requests pile up at the front.
That fixed outflow is the whole point, and it cuts both ways. A burst fills the queue with old requests, and if they aren't processed in time, recent requests get rate-limited behind them. There's no banked capacity to spend the way the token bucket allows, so a client that's been quiet earns nothing for it. The failure mode is starvation: queued old requests crowd out newer legitimate ones, and on a public endpoint, that means a real user waits behind a backlog generated by someone else's burst.
That's usually the wrong behavior for a public-facing API. Where the leaky bucket fits is shaping outbound or downstream traffic:
Protecting a fragile dependency that can only absorb so much per second.
Preventing a retry storm from overwhelming a downstream service.
Smoothing bursty outbound calls into a steady rate a partner API will accept.
The canonical example is an ecommerce platform metering writes into an inventory or order system that can only commit so many transactions per second. The shape matches: a steady drain into something that can't handle bursts.
Copy link to headingWhy stateless edge runtimes break every counter you trust
Every algorithm above assumes a counter you can read and write reliably. Serverless and edge functions are stateless, so an in-process counter resets the moment a new instance spins up. This is the part the textbook leaves out, and it's the part that decides whether your rate limiter works. You need a durable external store that persists state across instances rather than a variable in function memory, which on Vercel means something like Upstash Redis.
Copy link to headingRead latency makes it look like the perfect counter store
The classic mistake here is reaching for Edge Config to hold the counters, and I've watched more than one team make it because the read latency is seductive. Edge Config is built for reads at negligible latency, with the vast majority completing within 15ms at P99 and often under 1ms. That speed is exactly why it looks like the perfect counter store.
The problem is on the write side. Writes propagate globally in up to ten seconds, and the write propagation guidance is explicit that updates may take that long to reach every region, because Edge Config is built for values that change infrequently. A rate-limit counter updated thousands of times per second against a ten-second write window cannot enforce current request volume. It's not slow, it's the wrong tool: you're asking a configuration store to do a state store's job.
Copy link to headingSplit the two jobs
Edge Config still has a real job in rate limiting. The two roles split cleanly:
Read from Edge Config the configuration that changes rarely: per-tenant tier thresholds, feature flags, allow and deny lists. These hit at sub-millisecond latency.
Write to Upstash Redis the counters that change constantly. It's built for the edge and a serverless connection model rather than the long-lived sockets and connection pools traditional Redis setups expect.
Config you read. Counters you write somewhere that can take the write.
Copy link to headingAlgorithm selection, by traffic pattern
Start from the traffic you're serving and let that pick the algorithm, rather than starting from the algorithm and hoping your traffic cooperates. That's the order I run every time. The table below maps each algorithm to the shape it fits, the cost it carries, and where it's available on Vercel.
One caveat the table can't show, because it's a property of where you enforce rather than which algorithm you pick. Vercel WAF rate-limit counters are tracked per-region, and so are the @vercel/firewall SDK's counters. A client hitting multiple regions at once can exceed the configured limit in aggregate across regions.
This isn't a Vercel quirk, it's the physics of edge enforcement. Any system that decides at the edge without a per-request round-trip to a central store counts locally and reconciles late, so its counters lag real traffic by a few seconds. Zero-latency decisions and exact global counts are the two things you can't have at once.
You accept approximate global accuracy for low latency, or you route every decision through a single shared store and pay the synchronous round-trip on every request. Verify enforcement by testing it the way it fails: send concurrent traffic across regions and measure the aggregate, not a single-region burst.
Copy link to headingRate limiting contains noisy tenants but cannot stop bots
In a multi-tenant platform, one tenant's runaway traffic degrades everyone else unless you've keyed limits to contain it. The principle is fault isolation applied to load: when one workload spikes past its share, you reject that workload's excess and leave every other tenant running at predictable performance.
A global counter can't do this, because it pools everyone's requests into one number and starts rejecting whoever happens to arrive once the pool overflows, punishing the innocent for the noisy. The fix is to key the limit on tenant identity instead of on IP or a shared counter, and the @vercel/firewall SDK exposes exactly that through the rateLimitKey parameter:
const { rateLimited } = await checkRateLimit('update-object', { request, rateLimitKey: auth.orgId,});Using auth.orgId as the key contains one tenant's burst to that tenant. Combine that with per-tier thresholds read from Edge Config, and you get fair-use enforcement that scales with tenant count instead of collapsing the moment one tenant misbehaves.
Copy link to headingThe problem no counter can solve
The most expensive mistake in this piece is assuming rate limiting protects you from bots. It doesn't. It meters request counts, and we've watched that assumption fail in production. We documented an attack that came in through residential proxies, obscuring the real client IPs, and across hundreds of thousands of bot requests over two days, standard per-IP rate limits had nothing useful to act on. The limits were diluted across the fleet of IP addresses, and the real accounts passed authentication.
Rate limiting is a fair-use and cost-control tool. It counts requests without verifying whether a request came from a human, and an attacker who spreads load across enough identities never trips the counter.
This is why our architecture treats rate limiting and bot detection as separate, non-overlapping layers. BotID has no IP blocks, rate limits, or thresholds to tune by design. It deterministically validates session authenticity and returns a pass or fail rather than an ambiguous score.
Nous Research deployed BotID to block large-scale automated abuse and cut wasted inference costs without manual triage. You enable BotID to inspect incoming requests, and keep your WAF rules and rate limits as a separate layer you can configure and test independently.
Copy link to headingHow to choose between WAF rules and Routing Middleware
Rate limiting was never one decision, and the teams I've seen settle that before arguing about algorithms are the ones that get it right. It's three stacked decisions, and each belongs to a separate layer instead of one knob doing all three jobs.
The algorithm is chosen from your traffic pattern.
The enforcement location, chosen from your latency and accuracy tolerance.
The layer rate limiting can't cover at all, which is whether the caller is even human.
On Vercel, the enforcement-location decision comes down to two native paths:
WAF rules give you edge-layer enforcement with no code changes. They propagate globally in 300 milliseconds and take effect without a redeploy. You get fixed window on every plan and a token bucket on Enterprise. Reach for this when you want enforcement fast, and you can live with per-region counting.
Routing Middleware backed by Upstash Redis lets you run any algorithm on any plan. It reads tier configuration from Edge Config and writes counters to a durable store. Reach for this when you need an algorithm the WAF doesn't expose on your tier, or stronger global accuracy than per-region counting gives you.
No counter, on any algorithm, on either path, tells you whether the request was a bot. That's not a limit you tune your way out of, it's a different question that needs a different layer. Nous Research cut wasted inference costs by putting BotID ahead of rate limits, and the platform sustained 99.9992% uptime through BFCM 2024 with these layers working together rather than one of them stretched to cover all three jobs.
Copy link to headingFAQ
Copy link to headingWhy do my Vercel rate limits sometimes allow more than the configured limit?
WAF and @vercel/firewall counters are tracked per-region. A client hitting multiple edge regions can exceed the per-region limit in aggregate. This is the inherent tradeoff of edge enforcement: zero-latency decisions in exchange for approximate global accuracy. Route every decision through a single shared store for a hard global cap, and accept the round-trip cost on each request.
Copy link to headingCan I use a token bucket on the Pro plan?
The WAF exposes the token bucket only on Enterprise. On Hobby and Pro, the WAF provides a fixed window. Teams on those plans who want token-bucket or sliding-window semantics implement them in Routing Middleware with the @upstash/ratelimit SDK backed by Upstash Redis, which gives you any algorithm on any plan.
Copy link to headingShould I use Edge Config to store rate-limit counters?
No. Edge Config is built for low-latency reads of values that change infrequently, and its writes propagate globally in up to ten seconds. A counter updated thousands of times per second can't enforce the current volume against that write window. Use Edge Config to read tier thresholds and feature flags, and write counters to a durable store like Upstash Redis.
Copy link to headingWill rate limiting protect my API from bots?
No. Rate-limiting meters request counts and never verify whether a caller is human. An attacker spreading load across residential proxies dilutes per-IP limits until they have nothing to act on. Bot defense belongs in a separate, non-overlapping layer such as BotID, which validates session authenticity rather than counting requests.