import { readFileSync } from 'fs';
import { join } from 'path';
import type { Redis } from 'ioredis';
import { randomUUID } from 'crypto';
export interface LimitResult {
allowed: boolean;
remaining: number;
retryAfter: number;
}
export class RateLimiter {
private sha: string | null = null;
private readonly source = readFileSync(join(__dirname, 'slidingWindow.lua'), 'utf8');
constructor(private readonly redis: Redis) {}
async loadScript(): Promise<void> {
this.sha = (await this.redis.script('LOAD', this.source)) as string;
}
async check(key: string, limit: number, windowMs: number): Promise<LimitResult> {
if (!this.sha) await this.loadScript();
const now = Date.now();
const res = (await this.redis.evalsha(
this.sha as string,
1,
key,
String(now),
String(windowMs),
String(limit),
`${now}:${randomUUID()}`,
)) as [number, number];
return {
allowed: res[0] === 1,
remaining: res[1],
retryAfter: res[0] === 1 ? 0 : Math.ceil(windowMs / 1000),
};
}
}
-- KEYS[1] = rate limit key
-- ARGV[1] = now (ms), ARGV[2] = window (ms), ARGV[3] = limit, ARGV[4] = unique member id
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, member)
redis.call('PEXPIRE', key, window)
return { 1, limit - count - 1 }
end
return { 0, 0 }
import type { Request, Response, NextFunction, RequestHandler } from 'express';
import { RateLimiter } from './RateLimiter';
interface Options {
limiter: RateLimiter;
limit: number;
windowMs: number;
prefix: string;
keyGenerator?: (req: Request) => string;
}
function defaultKey(req: Request): string {
const userId = (req as any).user?.id ?? 'anon';
return `${req.ip}:${userId}`;
}
export function rateLimit(opts: Options): RequestHandler {
const keyFor = opts.keyGenerator ?? defaultKey;
return async (req: Request, res: Response, next: NextFunction) => {
try {
const key = `rl:${opts.prefix}:${keyFor(req)}`;
const result = await opts.limiter.check(key, opts.limit, opts.windowMs);
res.setHeader('X-RateLimit-Limit', opts.limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, result.remaining));
if (!result.allowed) {
res.setHeader('Retry-After', result.retryAfter);
return res.status(429).json({ error: 'Too many requests', retryAfter: result.retryAfter });
}
return next();
} catch (err) {
// Fail open: a Redis outage should not take down the API.
return next();
}
};
}
import express from 'express';
import Redis from 'ioredis';
import { RateLimiter } from './RateLimiter';
import { rateLimit } from './rateLimit';
const app = express();
app.set('trust proxy', 1); // required so req.ip reflects the real client behind a proxy
app.use(express.json());
const redis = new Redis(process.env.REDIS_URL as string);
const limiter = new RateLimiter(redis);
const globalLimit = rateLimit({ limiter, prefix: 'global', limit: 120, windowMs: 60_000 });
const loginLimit = rateLimit({
limiter,
prefix: 'login',
limit: 5,
windowMs: 5 * 60_000,
keyGenerator: (req) => `${req.ip}:${req.body?.email ?? 'unknown'}`,
});
app.use(globalLimit);
app.post('/auth/login', loginLimit, (req, res) => {
res.json({ ok: true });
});
app.get('/api/feed', (_req, res) => res.json({ items: [] }));
limiter.loadScript().then(() => app.listen(3000));
Rate limiting protects an API from abuse and accidental hammering, but a naive fixed-window counter (increment a key, expire it every 60 seconds) suffers from burst problems at window boundaries: a client can send the full quota at the end of one window and again at the start of the next, effectively doubling the allowed rate. This snippet implements a true sliding-window limiter backed by a Redis sorted set, keyed by both client IP and authenticated user so that anonymous traffic and logged-in traffic are throttled independently.
The slidingWindow.lua tab holds the core algorithm and runs atomically inside Redis. It uses one ZSET per key where each request is a member scored by timestamp. ZREMRANGEBYSCORE first evicts entries older than now - window, then ZCARD counts what remains. If the count is under the limit the script adds the current request with ZADD and refreshes the key's PEXPIRE; otherwise it rejects without inserting. Running this as a Lua script means the read-modify-write is a single round trip and cannot interleave with concurrent requests, which is what makes the counter correct under load.
The RateLimiter class tab wraps the script. loadScript primes the script with SCRIPT LOAD so subsequent calls use EVALSHA (avoiding resending the source each time), and check invokes it, returning a small result object with allowed, remaining, and a retryAfter hint derived from the window. Storing the SHA once is a common optimization that trades a tiny startup cost for cheaper hot-path calls.
The rateLimit middleware tab is the Express glue. It builds a composite key from req.ip and the optional req.user.id, so the same person behind one IP gets a shared IP budget plus a per-user budget. It sets the standard X-RateLimit-* and Retry-After headers and short-circuits with 429 when the limit is exceeded. keyGenerator and the numeric options are injectable so different routes can apply stricter limits.
The router wiring tab shows the pattern in practice: a permissive global limit plus a much tighter limit on the login route to blunt credential-stuffing. A key trade-off to remember is that trusting req.ip requires app.set('trust proxy', ...) behind a load balancer, otherwise every client collapses to the proxy's address.
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
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
Share this code
Here's the card — post it anywhere.