All posts
Node.jsAugust 11, 20268 min read

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.

F
Fepiq Team
Fepiq

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.

Why rate limit at the application layer

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.

Three algorithms, three trade-offs

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).

AlgorithmHow it worksBest forWeakness
Fixed windowCounts requests in a fixed time bucket (e.g. every 60s) with an INCR + EXPIRE pairSimple internal APIs, low request volumeBoundary burst — up to 2x the limit can slip through at the window edge
Sliding window logStores each request timestamp in a Redis sorted set and counts entries within the rolling windowAccurate, billable per-tenant limits on public SaaS APIsHigher memory per key at very high request rates
Token bucketTokens refill at a fixed rate; each request consumes one via an atomic Lua scriptBursty workloads (webhooks, batch imports) that should smooth out rather than hard-stopSlightly harder to explain in support tickets and billing dashboards

Implementing sliding-window limiting with Redis and Node.js

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:

  1. Design the key around what you're actually limiting — rate:{tenantId}:{route} gives you accurate, independent limits per tenant and per endpoint, instead of one global counter.
  2. Run the check as a single atomic Lua script: ZADD the current timestamp into the sorted set, ZREMRANGEBYSCORE to drop entries older than the window, ZCARD to count what's left, and EXPIRE the key to the window length — all in one round trip so concurrent requests can't race past the limit.
  3. Return standard headers on every response — RateLimit-Limit, RateLimit-Remaining, and Retry-After on a 429 — so client SDKs and integration partners can back off correctly instead of hammering you harder.
  4. Fail open, not closed. If Redis is unreachable, log the error and let the request through rather than taking your entire API down because the rate limiter's dependency hiccupped. A rate limiter outage should never become a full outage.

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.

Token bucket for bursty workloads

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.

Scaling considerations: hot keys and Redis under load

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.

  • For very high-volume shared keys, shard them — append a random suffix (0-9) at write time and sum across shards when reading — so the load spreads across the cluster instead of pinning one node.
  • Prefer Redis Cluster with tenant-scoped hash tags (rate:{tenant123}:route) so a given tenant's keys land predictably but different tenants naturally distribute across shards.
  • Run your rate limiter on a separate Redis instance or logical database from your session store and application cache, so a traffic spike against the limiter can't starve unrelated reads.
  • Pipeline the Lua script calls where possible, and keep connection pools sized to your Node.js cluster's worker count, not per-request — connection churn under load is a common, avoidable source of tail latency.

Multi-tenant patterns for SaaS APIs

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:

  • Key limits by API key or tenant ID, not IP, for authenticated traffic — it's the only identifier that maps cleanly to your billing plan.
  • Pull the actual numeric limit from tenant configuration at request time rather than hardcoding it, so upgrading a customer's plan takes effect immediately without a deploy.
  • Keep a separate, stricter IP-based limit on unauthenticated routes (login, signup, password reset) specifically to blunt credential-stuffing and scraping attempts.
  • Offer a burst allowance on top of the sustained rate for enterprise tiers — a short-lived higher ceiling handles legitimate spikes (bulk import, backfill jobs) without permanently raising the baseline limit.
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.
Fepiq Engineering

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 Spikes

Frequently asked questions

Should I use Redis or an in-memory store for Node.js rate limiting?+

In-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.

What's the difference between rate limiting and throttling in a Node.js API?+

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.

How do I rate limit by API key instead of IP address?+

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.

Does rate limiting hurt Node.js API performance?+

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.

What HTTP status code should a rate-limited request return?+

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 Fepiq

Get new posts in your inbox

Occasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.

Keep reading

Related posts

All posts

Let's build something

Ready to ship your next product with Fepiq?

Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.