-- KEYS[1] = current window key, KEYS[2] = previous window key
-- ARGV: limit, window_ms, elapsed_ms
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local elapsed = tonumber(ARGV[3])
local curr = tonumber(redis.call('GET', KEYS[1]) or '0')
local prev = tonumber(redis.call('GET', KEYS[2]) or '0')
local ratio = 1 - (elapsed / window)
if ratio < 0 then ratio = 0 end
local weighted = prev * ratio + curr
if weighted + 1 > limit then
local remaining = 0
return { 0, remaining, math.ceil((window - elapsed) / 1000) }
end
local newCurr = redis.call('INCR', KEYS[1])
-- keep two windows worth of data so the slide has history
redis.call('PEXPIRE', KEYS[1], window * 2)
local remaining = math.floor(limit - (prev * ratio + newCurr))
if remaining < 0 then remaining = 0 end
return { 1, remaining, 0 }
import { readFileSync } from 'fs';
import { join } from 'path';
import type { Redis } from 'ioredis';
export interface LimiterOptions {
limit: number;
windowMs: number;
prefix?: string;
}
export interface LimitResult {
allowed: boolean;
remaining: number;
retryAfter: number;
limit: number;
}
export class RedisRateLimiter {
private readonly script = readFileSync(join(__dirname, 'slidingWindow.lua'), 'utf8');
private sha: string | null = null;
constructor(private readonly redis: Redis, private readonly opts: LimiterOptions) {}
async check(identifier: string): Promise<LimitResult> {
const { limit, windowMs, prefix = 'rl' } = this.opts;
const now = Date.now();
const windowIndex = Math.floor(now / windowMs);
const elapsed = now - windowIndex * windowMs;
const currKey = `${prefix}:${identifier}:${windowIndex}`;
const prevKey = `${prefix}:${identifier}:${windowIndex - 1}`;
const [allowed, remaining, retryAfter] = await this.eval(
[currKey, prevKey],
[limit, windowMs, elapsed]
);
return { allowed: allowed === 1, remaining, retryAfter, limit };
}
private async eval(keys: string[], args: number[]): Promise<[number, number, number]> {
try {
if (!this.sha) this.sha = await this.redis.script('LOAD', this.script) as string;
return await this.redis.evalsha(this.sha, keys.length, ...keys, ...args) as [number, number, number];
} catch (err: any) {
if (String(err?.message).includes('NOSCRIPT')) {
this.sha = null;
return await this.redis.eval(this.script, keys.length, ...keys, ...args) as [number, number, number];
}
throw err;
}
}
}
import type { Request, Response, NextFunction, RequestHandler } from 'express';
import type { RedisRateLimiter } from './RedisRateLimiter';
type KeyFn = (req: Request) => string;
export function rateLimit(limiter: RedisRateLimiter, keyFn: KeyFn): RequestHandler {
return async (req: Request, res: Response, next: NextFunction) => {
const identifier = keyFn(req);
try {
const result = await limiter.check(identifier);
res.setHeader('X-RateLimit-Limit', result.limit);
res.setHeader('X-RateLimit-Remaining', result.remaining);
if (!result.allowed) {
res.setHeader('Retry-After', result.retryAfter);
return res.status(429).json({
error: 'rate_limited',
message: `Too many requests. Retry after ${result.retryAfter}s.`,
});
}
return next();
} catch (err) {
// Fail open: never let a Redis outage take down the route.
req.app.locals.logger?.error({ err, identifier }, 'rate limiter error');
return next();
}
};
}
export const ipKey: KeyFn = (req) =>
`ip:${req.ip ?? req.socket.remoteAddress ?? 'unknown'}`;
export const apiKey: KeyFn = (req) =>
`key:${(req.header('x-api-key') ?? 'anonymous').slice(0, 64)}`;
import { Router } from 'express';
import Redis from 'ioredis';
import { RedisRateLimiter } from './RedisRateLimiter';
import { rateLimit, ipKey, apiKey } from './rateLimit';
const redis = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379');
const loginLimiter = new RedisRateLimiter(redis, {
limit: 5,
windowMs: 60_000,
prefix: 'rl:login',
});
const searchLimiter = new RedisRateLimiter(redis, {
limit: 120,
windowMs: 60_000,
prefix: 'rl:search',
});
export const router = Router();
router.post('/auth/login', rateLimit(loginLimiter, ipKey), (req, res) => {
// ...credential verification
res.json({ ok: true });
});
router.get('/api/search', rateLimit(searchLimiter, apiKey), (req, res) => {
res.json({ results: [] });
});
This snippet implements a sliding-window rate limiter for Express, where the counting and decision logic live in Redis rather than in application memory. The core problem is fairness under bursty traffic: a naive fixed-window counter allows twice the intended rate at the boundary between two windows (a client can spend a full quota in the last second of window N and another full quota in the first second of window N+1). A sliding window smooths this by weighting the previous window's count by how far the current window has progressed, producing an approximate but boundary-safe estimate.
The slidingWindow.lua tab holds the atomic decision. It reads the current and previous bucket counters, computes a weighted count as prev * (1 - elapsed/window) + curr, and rejects the request if adding one would exceed limit. Doing this in a Lua script matters because it runs atomically on the Redis server, so the read-check-increment sequence cannot interleave with other concurrent requests — this is what prevents the classic race where two requests both read the same count and both pass. Both counter keys are given a TTL of two windows so stale buckets expire on their own without a cleanup job.
The RedisRateLimiter tab wraps that script. It computes the current window index and elapsed fraction in check, loads the script once and reuses its SHA (falling back to EVAL on NOSCRIPT), and returns a small typed result including retryAfter. Keys are namespaced per-identifier so different clients and routes never collide.
The rateLimit middleware tab adapts the limiter to Express. It resolves an identifier (an API key or the client IP), calls limiter.check, and either sets the standard X-RateLimit-* and Retry-After headers or responds 429. A key design choice is failing open: if Redis itself errors, the middleware logs and calls next() rather than blocking real traffic on an infrastructure hiccup — a deliberate availability-over-strictness trade-off.
The routes tab shows composition: a strict per-minute limit on /auth/login keyed by IP, and a more generous limit on /api/search keyed by API key. Because the limiter is injected, tests can swap in a fake, and multiple app instances share one source of truth through Redis.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.