Node.js Rate Limiting with Redis: A SaaS API Guide
Sliding window or token bucket? Compare Node.js rate limiting strategies with Redis, plus a step-by-step guide for scaling SaaS APIs safely.
Sliding window or token bucket? Compare Node.js rate limiting strategies with Redis, plus a step-by-step guide for scaling SaaS APIs safely.
If you're building a Node.js SaaS API and searching for "node.js rate limiting redis", you already know the two follow-up questions: which algorithm should you use, and how do you keep it fast once Redis itself is under load? The short answer: use a sliding-window log or token bucket backed by Redis sorted sets and a Lua script for atomicity, key your limits by tenant or API key rather than IP address, and shard high-volume keys so one noisy customer can't create latency spikes for everyone else. This guide walks through the trade-offs and a production-ready implementation for multi-tenant SaaS APIs.
Infrastructure-level throttling — an AWS ALB, CloudFront, or an API gateway — stops the most obvious floods, but it can't see your business logic. It doesn't know that a customer is on the Starter plan and should get 60 requests per minute while an Enterprise tenant gets 2,000. It can't distinguish a legitimate integration partner from a misconfigured script hammering your webhook endpoint. For a SaaS product, rate limiting is a billing and reliability feature as much as a security one: it protects downstream dependencies (email providers, PDF generation, third-party APIs) from being overwhelmed by a single tenant, and it lets you sell higher limits as part of a paid tier.
Most Node.js teams land on one of three approaches. Each has a clear best use case, and picking the wrong one shows up as either false positives (rejecting legitimate traffic) or false negatives (letting bursts through).
| Algorithm | How it works | Best for | Weakness |
|---|---|---|---|
| Fixed window | Counts requests in a fixed time bucket (e.g. every 60s) with an INCR + EXPIRE pair | Simple internal APIs, low request volume | Boundary burst — up to 2x the limit can slip through at the window edge |
| Sliding window log | Stores each request timestamp in a Redis sorted set and counts entries within the rolling window | Accurate, billable per-tenant limits on public SaaS APIs | Higher memory per key at very high request rates |
| Token bucket | Tokens refill at a fixed rate; each request consumes one via an atomic Lua script | Bursty workloads (webhooks, batch imports) that should smooth out rather than hard-stop | Slightly harder to explain in support tickets and billing dashboards |
For most SaaS APIs, a sliding-window log gives the best accuracy-to-complexity ratio. The pattern is the same whether you use ioredis or node-redis:
The Lua script matters more than it looks. Node.js's event loop means two requests for the same tenant can both read the current count before either one writes back — a classic race condition. Wrapping the read-check-write sequence in a single Lua script (or a WATCH/MULTI/EXEC transaction) makes the whole operation atomic on the Redis side, so the limit holds even under concurrent load from multiple Node.js instances.
Sliding windows are strict: hit the limit and every next request fails until entries age out. That's wrong for legitimately bursty patterns — a customer triggering a CSV import, or a webhook retry storm after your API had a brief blip. A token bucket, stored as a Redis hash holding a token count and a last-refill timestamp, refills at a steady rate (say, 10 tokens per second up to a cap of 100) and lets a request through as long as a token is available. The refill math runs inside a Lua script too, so bursts are absorbed smoothly instead of being cut off at a hard wall, while sustained abuse still gets throttled once the bucket runs dry.
A rate limiter is one of the easiest ways to accidentally create a Redis hot key. If your key design is coarse — say, a single global rate:signup key protecting an unauthenticated endpoint — every request from every user across every region lands on the same Redis shard, and that shard's CPU and network throughput becomes your API's ceiling. Symptoms show up as latency spikes that seem to hit unrelated requests, because they're queued behind the same hot key on the same node.
Generic IP-based limiting is a poor fit for B2B SaaS, where dozens of a customer's employees can sit behind the same corporate NAT and share one IP. A few patterns work better in production:
“Rate limiting isn't about saying no to your users — it's about guaranteeing yes to all of them, consistently, even when one tenant tries to take more than their share.”
Rate-limiter keys are a classic source of Redis hot-key contention once you're at scale. See our deep dive on diagnosing and fixing it.
Read: Fixing Redis Hot Key Latency SpikesIn-memory counters only work if your API runs as a single process, which almost no production SaaS does. Once you have multiple Node.js instances behind a load balancer, each process has its own counter and the real limit becomes instances × your intended limit. Redis gives every instance a shared, consistent view of the count.
Rate limiting rejects or queues requests once a threshold is crossed, typically returning a 429 status. Throttling instead slows the rate at which requests are processed or responses are sent, without necessarily rejecting anything. Production SaaS APIs often combine both: throttle gently as usage climbs, then hard rate limit past a defined ceiling.
Extract the tenant or API key during authentication middleware, before the rate limit check runs, and use it as part of the Redis key (rate:{apiKey}:{route}). This keeps limits tied to a billing entity rather than a network address, which is what you actually want to enforce for paid tiers.
A well-implemented Redis-backed limiter using a single pipelined Lua script typically adds only 1-2ms of latency per request, which is negligible compared to the protection it provides against cascading overload. The bigger performance risk is a poorly sharded key design that creates Redis hot keys, not the rate limiting logic itself.
Return 429 Too Many Requests, along with a Retry-After header telling the client how many seconds to wait. Well-behaved SDKs and integration partners read this header automatically to back off, which reduces the retry storm that would otherwise make the overload worse.
Need a Node.js and Redis architecture that stays fast as your SaaS scales?
Talk to FepiqOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
What is an index in SQL? A plain-English guide with copy-paste examples showing how indexes speed up queries and when you actually need one.
Learn JavaScript DOM manipulation for beginners: select elements, change text and styles, and handle clicks with simple, copy-paste code examples.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.