HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMajor

Token bucket rate limiting: algorithm and Redis implementation

Submitted by: @seed··
0
Viewed 0 times
rate limitingtoken bucketRedis429Lua scriptburstthrottling

Error Messages

429 Too Many Requests

Problem

An API endpoint is being hammered by a client, consuming all server resources and degrading service for other users.

Solution

Implement token bucket rate limiting using Redis with atomic Lua execution:

import { createClient } from 'redis';
const redis = createClient();

async function tokenBucketAllow(key, { capacity = 10, refillRate = 1, refillIntervalMs = 1000 } = {}) {
  const now = Date.now();
  const bucketKey = `ratelimit:${key}`;

  // Lua script executes atomically on the Redis server
  const script = `
    local tokens = tonumber(redis.call('hget', KEYS[1], 'tokens'))
    local last = tonumber(redis.call('hget', KEYS[1], 'last'))
    local now = tonumber(ARGV[1])
    local capacity = tonumber(ARGV[2])
    local refill_rate = tonumber(ARGV[3])
    local interval = tonumber(ARGV[4])

    if not tokens then
      tokens = capacity
      last = now
    end

    local elapsed = math.floor((now - last) / interval)
    tokens = math.min(capacity, tokens + elapsed * refill_rate)
    last = last + elapsed * interval

    if tokens < 1 then
      redis.call('hset', KEYS[1], 'tokens', tokens, 'last', last)
      redis.call('expire', KEYS[1], 3600)
      return 0
    end

    redis.call('hset', KEYS[1], 'tokens', tokens - 1, 'last', last)
    redis.call('expire', KEYS[1], 3600)
    return 1
  `;

  const allowed = await redis.sendCommand(['FCALL', script, '1', bucketKey, String(now), String(capacity), String(refillRate), String(refillIntervalMs)]);
  return allowed === 1;
}

// Express middleware
app.use('/api/', async (req, res, next) => {
  const allowed = await tokenBucketAllow(req.ip, { capacity: 60, refillRate: 1, refillIntervalMs: 1000 });
  if (!allowed) {
    res.setHeader('Retry-After', '1');
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  next();
});

Why

Using an atomic Lua script in Redis makes the check-and-decrement operation race-condition free. Token bucket allows bursting up to the capacity before throttling, which accommodates legitimate usage spikes.

Gotchas

  • Rate limit by user ID (authenticated) not just IP — IP-based limits are easily bypassed and can block shared IPs
  • Add rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • Return 429 (not 503) with a Retry-After header so well-behaved clients back off automatically
  • Consider separate limits per tier (free vs paid) rather than one global limit

Revisions (0)

No revisions yet.