DEV Community

1xApi
1xApi

Posted on • Originally published at 1xapi.com

How to Implement Redis Token Bucket Rate Limiting in Node.js (2026 Guide)

How to Implement Redis Token Bucket Rate Limiting in Node.js (2026 Guide)

As of March 2026, API rate limiting remains one of the most critical components of production API design. Whether you're protecting against abuse, ensuring fair usage, or preventing downstream system failures, a robust rate limiting strategy is essential. In this guide, we'll implement a production-ready token bucket rate limiter using Redis and Node.js.

Why Token Bucket?

The token bucket algorithm has become the preferred choice for modern APIs because it handles bursty traffic elegantly. Unlike simple fixed-window limiters that can allow traffic spikes at window boundaries, token bucket allows controlled bursts while maintaining a consistent long-term average.

Key advantages:

  • Burst-friendly: Clients can send multiple requests at once if they've accumulated tokens
  • Smooth enforcement: No sharp cutoffs at window boundaries
  • Flexible configuration: Separate controls for burst capacity and refill rate

Prerequisites

npm install express ioredis lua
Enter fullscreen mode Exit fullscreen mode

We'll use:

  • Express.js (v4.21) - Web framework
  • ioredis (v5.x) - Redis client
  • Lua scripts - For atomic operations

The Architecture

Here's how the token bucket works:

  1. Each client has a "bucket" with tokens (max capacity = burst limit)
  2. Tokens refill at a steady rate (refill rate)
  3. Each request consumes 1 token
  4. If bucket is empty → 429 Too Many Requests

Implementation

1. Redis Lua Script (Atomic Operations)

The key to a reliable rate limiter is atomicity. We'll use a Lua script to ensure token checking and consumption happens in a single Redis operation:

// rate-limiter.lua
local key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local refillRate = tonumber(ARGV[3])

local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1])
local lastRefill = tonumber(bucket[2])

-- Initialize if doesn't exist
if tokens == nil then
    tokens = capacity
    lastRefill = now
end

-- Calculate token refill
local elapsed = now - lastRefill
local refillAmount = elapsed * refillRate
tokens = math.min(capacity, tokens + refillAmount)
lastRefill = now

-- Check if request is allowed
local allowed = false
if tokens >= 1 then
    tokens = tokens - 1
    allowed = true
end

-- Save back to Redis with 24h expiry
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', lastRefill)
redis.call('EXPIRE', key, 86400)

return {allowed and 1 or 0, tokens}
Enter fullscreen mode Exit fullscreen mode

2. Rate Limiter Class

// rate-limiter.js
const Redis = require('ioredis');
const fs = require('fs');
const path = require('path');

class TokenBucketRateLimiter {
    constructor(options = {}) {
        this.redis = options.redis || new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
        this.capacity = options.capacity || 100;        // Max tokens in bucket
        this.refillRate = options.refillRate || 10;       // Tokens per second
        this.keyPrefix = options.keyPrefix || 'ratelimit';

        // Load Lua script
        this.luaScript = fs.readFileSync(
            path.join(__dirname, 'rate-limiter.lua'),
            'utf8'
        );
    }

    /**
     * Check if request is allowed and consume a token
     * @param {string} identifier - Client identifier (IP, userId, API key)
     * @returns {Promise<{allowed: boolean, remaining: number, resetIn: number}>}
     */
    async check(identifier) {
        const key = `${this.keyPrefix}:${identifier}`;
        const now = Date.now();

        const result = await this.redis.eval(
            this.luaScript,
            1,                      // Number of keys
            key,                    // Key name
            now,                    // ARGV[1]: current timestamp
            this.capacity,          // ARGV[2]: bucket capacity
            this.refillRate / 1000  // ARGV[3]: refill rate per ms
        );

        const allowed = result[0] === 1;
        const remaining = Math.floor(result[1]);

        // Calculate reset time (time to get 1 token when bucket is empty)
        const resetIn = allowed ? 0 : Math.ceil((1 - remaining) / (this.refillRate / 1000) * 1000);

        return {
            allowed,
            remaining: Math.max(0, remaining),
            resetIn,
            limit: this.capacity
        };
    }

    /**
     * Express middleware factory
     */
    middleware(options = {}) {
        const limiter = this;
        const {
            getIdentifier = (req) => req.ip || req.connection.remoteAddress,
            skipSuccessfulRequests = false,
            skipFailedRequests = false
        } = options;

        return async (req, res, next) => {
            const identifier = getIdentifier(req);
            const result = await limiter.check(identifier);

            // Set rate limit headers
            res.set({
                'RateLimit-Limit': result.limit,
                'RateLimit-Remaining': result.remaining,
                'RateLimit-Reset': Math.ceil(result.resetIn / 1000)
            });

            if (!result.allowed) {
                res.set('Retry-After', Math.ceil(result.resetIn / 1000));
                return res.status(429).json({
                    error: 'Too Many Requests',
                    message: 'Rate limit exceeded. Please try again later.',
                    retryAfter: result.resetIn
                });
            }

            next();
        };
    }

    async close() {
        await this.redis.quit();
    }
}

module.exports = TokenBucketRateLimiter;
Enter fullscreen mode Exit fullscreen mode

3. Express Server Integration

// server.js
const express = require('express');
const TokenBucketRateLimiter = require('./rate-limiter');

const app = express();
const PORT = process.env.PORT || 3000;

// Create rate limiter instance
const limiter = new TokenBucketRateLimiter({
    capacity: 100,        // Allow 100 requests
    refillRate: 10,       // Refill 10 tokens per second
    keyPrefix: 'api'
});

// Apply to all routes
app.use(limiter.middleware({
    getIdentifier: (req) => {
        // Use API key if available, otherwise fall back to IP
        return req.headers['x-api-key'] || req.ip;
    }
}));

// Example endpoints
app.get('/api/data', (req, res) => {
    res.json({ message: 'Data retrieved successfully' });
});

app.post('/api/submit', (req, res) => {
    res.json({ message: 'Data submitted successfully' });
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Advanced: Endpoint-Specific Limits

Different endpoints often need different limits:

const rateLimitConfigs = {
    '/api/auth/login': { capacity: 5, refillRate: 1 },    // Strict: 5/min
    '/api/data': { capacity: 100, refillRate: 10 },        // Normal: 100/s
    '/api/reports': { capacity: 10, refillRate: 1 }        // Heavy: 10/s
};

app.use((req, res, next) => {
    const path = req.path;
    const config = rateLimitConfigs[path] || { capacity: 100, refillRate: 10 };

    const limiter = new TokenBucketRateLimiter({
        ...config,
        keyPrefix: `api:${path}`
    });

    limiter.middleware({ getIdentifier: (req) => req.ip })(req, res, next);
});
Enter fullscreen mode Exit fullscreen mode

Best Practices for 2026

  1. Fail Open: If Redis goes down, allow requests rather than blocking all traffic
  2. Use Standard Headers: Always include RateLimit-* headers for client visibility
  3. Log & Monitor: Track rate limit hits to detect abuse patterns
  4. Separate Limits by Tier: Free users get stricter limits than paid users

Conclusion

Token bucket rate limiting with Redis provides a robust, scalable solution for API traffic management. The Lua script ensures atomic operations under high concurrency, and the modular design allows easy integration with any Express application.

At 1xAPI, we understand the importance of rate limiting for API reliability. Whether you're building public APIs or internal services, implementing proper rate limiting protects your infrastructure while providing a fair experience for all users.

Top comments (0)