Skip to content

Redis Rate Limiting — Complete Implementation Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Redis Rate Limiting. We cover key concepts, practical examples, and best practices to help you master this topic.

Redis rate limiting leverages Redis atomic operations, TTL-based expiry, and Lua scripting to implement fast, accurate, and distributed rate limiting that scales across multiple application servers.

What You'll Learn

By the end of this tutorial, you will implement Redis-based rate limiting using INCR, sorted sets, and Lua scripts, and understand when to use each pattern.

Why It Matters

In-memory rate limiting breaks in multi-server deployments. Redis provides a shared, atomic counter store that all application instances can use consistently.

Real-World Use

DodaTech's Microservices all share a single Redis cluster for rate limiting, ensuring consistent limits regardless of which service instance handles a request.

Redis Rate Limiting Learning Path

flowchart LR
  A[Express Rate Limiting] --> B[Redis Rate Limiting]
  B --> C[INCR + EXPIRE]
  B --> D[Sorted Sets]
  B --> E[Lua Scripts]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

INCR with EXPIRE Pattern

The simplest Redis rate limiting pattern uses INCR to increment a counter and EXPIRE to set a TTL, creating an automatic fixed window.

const Redis = require("ioredis");
const redis = new Redis();

async function incrRateLimit(req, res, next) {
  const key = `ratelimit:${req.ip}:${Math.floor(Date.now() / 60000)}`;
  const max = 10;

  const count = await redis.incr(key);

  if (count === 1) {
    await redis.expire(key, 120);
  }

  res.setHeader("X-RateLimit-Limit", max);
  res.setHeader("X-RateLimit-Remaining", Math.max(0, max - count));
  res.setHeader("X-RateLimit-Reset", Math.floor(Date.now() / 60000) * 60000 + 60000);

  if (count > max) {
    return res.status(429).json({ error: "Rate limit exceeded" });
  }

  next();
}

Expected behavior: Each minute window has a counter in Redis. Atomic INCR ensures accuracy. TTL of 120 seconds cleans up automatically.

Sorted Set Sliding Window

For precise sliding window rate limiting, Redis sorted stores timestamps as scores, enabling range queries to count requests in any time window.

async function sortedSetLimiter(req, res, next) {
  const key = `ratelimit:ss:${req.ip}`;
  const windowMs = 60000;
  const max = 10;
  const now = Date.now();
  const member = `${req.ip}:${now}:${Math.random()}`;

  const count = await redis.zcount(key, now - windowMs, "+inf");

  if (count >= max) {
    const oldest = await redis.zrange(key, 0, 0, "WITHSCORES");
    const retryAfter = Math.ceil((parseInt(oldest[1]) + windowMs - now) / 1000);
    return res.status(429).json({
      error: "Too many requests",
      retryAfter
    });
  }

  await redis.multi()
    .zadd(key, now, member)
    .zremrangebyscore(key, 0, now - windowMs)
    .expire(key, Math.ceil(windowMs / 1000) + 1)
    .exec();

  next();
}

Lua Script for Atomic Operations

Lua scripts ensure multiple Redis operations execute atomically, preventing race conditions in high-concurrency scenarios.

-- rate_limit.lua
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local max = tonumber(ARGV[3])
local member = ARGV[4]

redis.call("ZREMRANGEBYSCORE", key, 0, now - window)
local count = redis.call("ZCARD", key)

if count < max then
  redis.call("ZADD", key, now, member)
  redis.call("EXPIRE", key, math.ceil(window / 1000))
  return {1, count + 1}
else
  local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")
  return {0, count, oldest[2]}
end
async function luaLimiter(req, res, next) {
  const key = `ratelimit:lua:${req.ip}`;
  const now = Date.now();
  const member = `${now}:${Math.random().toString(36).slice(2)}`;

  const result = await redis.eval(
    script, 1, key, now, 60000, 10, member
  );

  if (result[0] === 0) {
    const retryAfter = Math.ceil((parseInt(result[2]) + 60000 - now) / 1000);
    return res.status(429).json({
      error: "Rate limit exceeded",
      retryAfter
    });
  }

  next();
}

Common Mistakes

  1. Not setting EXPIRE on keys -- Without TTL, Redis memory fills with stale keys. Always set EXPIRE after INCR for fixed window.

  2. Using non-atomic operations -- GET, check, SET in separate commands creates race conditions. Use INCR, Lua, or MULTI/EXEC.

  3. Hardcoding Redis connection details -- Use environment variables or a configuration service for Redis connection parameters.

  4. Not handling Redis connection failures -- When Redis is down, rate limiting stops. Implement fallback logic or fail open.

  5. Creating too many unique keys -- Each unique client creates a Redis key. Millions of keys consume memory. Use appropriate key expiry.

Practice Questions

  1. Why is INCR preferred over GET + SET for rate limiting? INCR is atomic. GET + SET has a Race Condition where two requests can read the same value.

  2. How does sorted set sliding window avoid storing all timestamps indefinitely? The ZREMRANGEBYSCORE command removes entries outside the window on every request.

  3. What happens if a Lua script takes too long? Redis blocks all other operations while the script runs. Keep Lua scripts fast and avoid infinite loops.

  4. Challenge: Implement a rate limiter that distinguishes between burst and sustained limits using Redis.

// Allow a burst of 20 requests, then 10 requests per minute
const burstKey = `burst:${req.ip}`;
const sustainedKey = `sustained:${req.ip}`;
const burstCount = await redis.get(burstKey);
if (burstCount < 20) {
  await redis.incr(burstKey);
  await redis.expire(burstKey, 60);
} else {
  // Enforce sustained limit
}

FAQ

How much memory does Redis rate limiting use?

Each key uses approximately 100 bytes plus the member data. For 100K clients, this uses about 10MB, which is negligible for Redis.

Can I use Redis Cluster for rate limiting?

Yes, but ensure all operations for the same key go to the same node. Use hash tags {key} to guarantee slot affinity.

What is the latency overhead of Redis rate limiting?

A single Redis INCR or sorted set operation takes 0.5-2ms on a local network. Lua scripts add minimal overhead for multiple operations.

Should I use Redis for rate limiting or a dedicated service?

Redis is sufficient for most applications. Dedicated rate limiting services (like Kong or custom services) are needed for very complex policies.

How do I handle Redis failover?

Use Redis Sentinel or Redis Cluster for high availability. Implement client-side fallback to memory limiting during failover.

Mini Project

Build a complete Redis rate limiting system with Lua scripts, key management, connection pooling, and graceful degradation.

const Redis = require("ioredis");

class RedisRateLimiter {
  constructor(opts) {
    this.redis = new Redis({
      host: opts.redisHost || "localhost",
      port: opts.redisPort || 6379,
      enableOfflineQueue: false,
      retryStrategy: (times) => Math.min(times * 50, 2000)
    });

    this.script = `
      local k = KEYS[1]
      local n = tonumber(ARGV[1])
      local w = tonumber(ARGV[2])
      local m = tonumber(ARGV[3])
      local mb = ARGV[4]
      redis.call("ZREMRANGEBYSCORE", k, 0, n - w)
      local c = redis.call("ZCARD", k)
      if c < m then
        redis.call("ZADD", k, n, mb)
        redis.call("EXPIRE", k, math.ceil(w / 1000) + 1)
        return {1, c + 1, m}
      end
      return {0, c, m}
    `;
  }

  async check(key, opts) {
    try {
      const now = Date.now();
      const member = `${now}:${Math.random().toString(36).slice(2)}`;
      const result = await this.redis.eval(
        this.script, 1,
        `ratelimit:${key}`,
        now, opts.windowMs || 60000,
        opts.max || 100,
        member
      );
      return { allowed: result[0] === 1, current: result[1], limit: result[2] };
    } catch (err) {
      console.error("Rate limiter error:", err.message);
      return { allowed: true, current: 0, limit: opts.max || 100 };
    }
  }
}

What's Next

Now that you understand Redis rate limiting, explore rate limiting across multiple servers. Then learn about proper rate limit HTTP headers.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro