javascript 107 lines · 3 tabs

Token Bucket Rate Limiter as Express Middleware

Shared by codesnips Jul 2026
3 tabs
class TokenBucket {
  constructor(capacity, refillRatePerSec) {
    this.capacity = capacity;
    this.refillRate = refillRatePerSec;
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  refill() {
    const now = Date.now();
    const elapsedSec = (now - this.lastRefill) / 1000;
    if (elapsedSec <= 0) return;
    const added = elapsedSec * this.refillRate;
    if (added > 0) {
      this.tokens = Math.min(this.capacity, this.tokens + added);
      this.lastRefill = now;
    }
  }

  tryRemove(cost = 1) {
    this.refill();
    if (this.tokens >= cost) {
      this.tokens -= cost;
      return true;
    }
    return false;
  }

  retryAfter(cost = 1) {
    this.refill();
    const deficit = cost - this.tokens;
    if (deficit <= 0) return 0;
    return Math.ceil(deficit / this.refillRate);
  }

  isFull() {
    this.refill();
    return this.tokens >= this.capacity;
  }
}

module.exports = TokenBucket;
3 files · javascript Explain with highlit

This snippet implements per-client rate limiting using the token bucket algorithm, a classic approach that allows short bursts while capping the long-run request rate. A bucket holds up to capacity tokens and refills at a steady refillRate per second; each request costs one token, and when the bucket is empty the request is rejected. Unlike a fixed window counter, the token bucket smooths traffic and avoids the boundary spike where a client fires two full windows worth of requests across a window edge.

The TokenBucket class in the first tab is deliberately lazy: instead of running a timer to add tokens, refill() computes how many tokens should have accrued since lastRefill based on elapsed time. This means an idle bucket costs nothing until it is touched again, which matters when tracking thousands of clients. tryRemove() refills first, then removes a token only if one is available, returning a boolean the middleware uses to allow or deny. The helper retryAfter() reports how many seconds until the next token becomes available so callers can back off intelligently.

The rateLimiter middleware tab wires the bucket into Express. It keeps a Map of buckets keyed by a keyGenerator function, defaulting to the client IP, so each caller gets an independent quota. A periodic sweep in setInterval evicts buckets that are full and stale, preventing unbounded memory growth from one-off clients; the timer is unref()ed so it never keeps the process alive on its own. On each request the middleware sets the standard X-RateLimit-* headers, and on rejection it also sets Retry-After and responds 429.

The app.js tab shows the middleware applied globally with a tight limit and then overridden on a hot login route with a stricter bucket, demonstrating that limits compose per-router. Trade-offs are worth noting: this store is in-memory and per-process, so behind multiple instances each process enforces its own quota — a shared store like Redis is needed for a global limit. The IP default is also naive behind proxies, where req.ip should be trusted only when trust proxy is configured. Still, for a single node or as a coarse first line of defense, the in-memory token bucket is fast, dependency-free, and easy to reason about.


Related snips

Share this code

Here's the card — post it anywhere.

Token Bucket Rate Limiter as Express Middleware — share card
Link copied