Open Source · MIT License

Rate limiting
for Node.js.

Production-grade distributed rate limiting for Node.js. Atomic Lua scripts, Redis Cluster-safe, three battle-tested algorithms. Express and Next.js adapters included.

View on GitHub
3 algorithmsRedis ClusterAtomic LuaTypeScriptExpress + Next.js
0Algorithmssliding-window-counter, log, token-bucket
0Redis round-tripEVALSHA for atomic, zero-race execution
0Dependenciesioredis is optional peer dep only
0%TypeScriptStrict mode, typed events, full .d.ts
Three algorithms

Pick the right tool for your traffic.

All three run as atomic Lua scripts on Redis — one round-trip, no race conditions. Switch algorithms with a single option change.

Cloudflare formula

Sliding Window Counter

Approximates a true sliding window by weighting the previous window's count. Error bounded at ~0.1% at the window boundary.

count = floor(prev × weight) + current
  • 2 Redis keys (hash-tagged, same slot)
  • Single EVALSHA round-trip
  • Cluster-safe atomic update

When to use

High throughput APIs, predictable billing, rate card enforcement.

const r = await limiter.check({
  key: 'user:42',
  limit: 1000,
  windowMs: 60_000,
})
Exact accounting

Sliding Window Log

Stores every request timestamp in a sorted set. Exact count of requests in the last N milliseconds — no approximation.

count = |{t : now − window ≤ t ≤ now}|
  • 1 Redis sorted set per key
  • ZREMRANGEBYSCORE + ZADD + ZCARD
  • Unique nonce prevents same-ms dedup

When to use

Billing systems, compliance, low-volume APIs where precision is required.

const r = await limiter.check({
  key: 'api:tenant:9',
  limit: 100,
  windowMs: 3_600_000, // 1h
  // algorithm: 'sliding-window-log'
})
Burst-friendly

Token Bucket

Tokens refill at a fixed rate. Allows short bursts up to the bucket capacity while smoothing long-term throughput.

tokens = min(limit, prev + elapsed × rate)
  • 1 Redis hash per key (tokens + lastRefill)
  • Continuous token refill via elapsed time
  • No burst penalty on cold start

When to use

Webhooks, streaming APIs, any use-case where occasional bursts are acceptable.

const r = await limiter.check({
  key: 'webhook:sender',
  limit: 50,         // max burst
  windowMs: 60_000,  // refill period
  // algorithm: 'token-bucket'
})
Features

Production-ready out of the box.

Every detail you'd need to ship reliable rate limiting — from cluster safety to real-time observability.

Atomic Lua Scripts

All state mutations happen in a single EVALSHA call — one Redis round-trip, zero race conditions between INCR and EXPIRE.

Redis Cluster Safe

Hash-tagged keys keep both window slots on the same slot. {user:42}:sw:... — the cluster router never splits a Lua script across nodes.

NOSCRIPT Recovery

When Redis restarts and flushes the script cache, FloodGate catches NOSCRIPT, re-loads via SCRIPT LOAD, and retries — completely transparent to callers.

TypeScript First

Strict mode throughout. Typed EventEmitter with discriminated unions for check, blocked, redis:error, and redis:fallback events.

Express & Next.js

Drop-in middleware for Express with RateLimit-* headers. App Router handler wrapper and Next.js middleware.ts helper for edge runtime.

Real-time Dashboard

Next.js 15 SSE-powered observability dashboard. Per-key request counts, block rates, a 60-second sparkline, and live traffic simulation.

Examples

Start in two minutes.

import { createLimiter } from 'floodgate-rl'

// In-memory — zero infra, perfect for single-process or tests
const limiter = createLimiter({ backend: 'memory' })

const result = await limiter.check({
  key: 'user:42',
  limit: 100,
  windowMs: 60_000,  // 1 minute
})

// { allowed: true, remaining: 99, resetAt: 1700000060000 }
console.log(result.allowed, result.remaining)
Architecture

One request. One round-trip.

Every check is a single atomic operation. No multi-step transactions, no lost updates.

RequestYour API receives a request
FloodGateCalls backend.check()
Redis / LuaEVALSHA — atomic, one trip
Allow / BlockHeaders set, retryAfter optional

Edge case handling

NOSCRIPT?SCRIPT LOAD → EVALSHA retry
Redis Error?Emit redis:error → fallback to memory
Cluster?{key}:sw:ts on same slot always
// EVALSHA with automatic NOSCRIPT retry try { await redis.evalsha(sha, keys, args) } catch { if (err.message.startsWith('NOSCRIPT')) { const sha = await redis.script('LOAD', lua) // reload return await redis.evalsha(sha, keys, args) // retry } }

Start in
two minutes.

No setup required for the memory backend. Add Redis when you're ready to distribute.

MIT licensed · TypeScript · Node.js 20+ · No vendor lock-in