Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AmeyaBorkar/throttlekit/llms.txt

Use this file to discover all available pages before exploring further.

A conventional per-key rate limiter allocates one record per active key. Under a volumetric DDoS attack from millions of distinct source IPs, that per-key state is itself the memory-exhaustion vector — the attacker doesn’t need to exceed your rate limit, they just need to send enough unique keys to exhaust your process heap. sketchRateLimit solves this by replacing the per-key map with a single Count-Min Sketch (CMS) whose memory footprint depends only on the accuracy parameters, never on how many distinct keys are seen.

sketchRateLimit()

import { sketchRateLimit } from "throttlekit";

const limiter = sketchRateLimit({
  limit: 100,
  windowMs: 60_000,
  // epsilon/delta trade accuracy for memory (defaults shown):
  epsilon: 0.01,   // at most epsilon*N overcount with probability >= 1-delta
  delta: 0.001,
});

console.log("fixed capacity:", limiter.capacity, "counters"); // never grows

const d = limiter.checkSync("192.0.2.1");
sketchRateLimit is @experimental — excluded from the 1.x SemVer guarantee; the interface may change in a minor release.

SketchRateLimitOptions

limit
number
required
Maximum requests admitted per key within each window. A hard ceiling — never exceeded.
windowMs
number
required
Window width in milliseconds. Windows are epoch-aligned: floor(now / windowMs) * windowMs.
epsilon
number
Additive accuracy. The sketch overestimates a key’s count by at most epsilon × N (where N is the total admitted mass in the window) with probability >= 1 - delta. Smaller is more accurate but uses more memory (width = ceil(e / epsilon)). Default 0.01.
delta
number
Failure probability for the epsilon bound. Smaller means more reliable but uses more memory (depth = ceil(ln(1 / delta))). Default 0.001.
conservative
boolean
Use the Estan–Varghese conservative-update rule to tighten the overestimate while preserving the never-underestimate guarantee. Default true.
seed
number
32-bit hash seed. Defaults to a per-instance random value so an attacker cannot precompute keys that collide with a victim and grief it into false denial. Only override with a fixed seed in tests.
clock
Clock
Injected time source. Defaults to the system clock.

SketchRateLimiter Interface

interface SketchRateLimiter {
  checkSync(key: string, cost?: number): Decision;
  check(key: string, cost?: number): Promise<Decision>;
  reset(): void;
  readonly capacity: number; // counter count (depth × width); fixed for the limiter's life
}
capacity is the total number of counters (depth × width). At default parameters (epsilon=0.01, delta=0.001), this is approximately 1,850 counters × 4 bytes = ~7.4 KB, regardless of how many keys are observed.
cost must be a positive integer. The counters are Uint32Array, so a fractional cost would truncate and break the never-over-admit guarantee. Passing a non-integer cost throws a RangeError.

The Safety Guarantee

The guarantee is hard and non-probabilistic:
Because estimate(key) >= trueCount(key) always, an allowed decision implies the true admitted count for that key is <= limit.
The limiter never over-admits. Its only error is in the safe direction: it may deny a key slightly early once hash collisions inflate its estimate. By the CMS bound that early-denial probability is bounded by epsilon × N (the total admitted mass in the window) with probability >= 1 - delta. Over-denying (never over-admitting) is exactly the right bias for DDoS and abuse protection: a false-positive denial for a legitimate key is tolerable; a false-negative that lets a flood through is not.

When to Use

ScenarioRecommended limiter
High-value API keys, billing, authrateLimit (exact, per-key store)
Public API with trusted, bounded key spacerateLimit with l1.maxKeys
DDoS mitigation — millions of distinct source IPssketchRateLimit
Public endpoint with untrusted, unbounded key universesketchRateLimit or twoTier with l1.maxKeys
Cluster-wide heavy-hitter detection (best-effort)mergeableSketch

Full Example

import { ManualClock, sketchRateLimit } from "throttlekit";

// ── 1. Fixed memory regardless of key count ──────────────────────────────────
function exactWithinBudget(): void {
  const clock = new ManualClock(0);
  const limiter = sketchRateLimit({ limit: 5, windowMs: 1_000, clock });

  console.log(`capacity: ${limiter.capacity} counters (~7.4 KiB), fixed for life`);

  // 1,000 distinct IPs, each making one request, are all admitted.
  // Memory has not grown.
  let admitted = 0;
  for (let i = 0; i < 1_000; i++) {
    if (limiter.checkSync(`198.51.100.${i}`).allowed) admitted++;
  }
  console.log(`1,000 distinct IPs → ${admitted} admitted; capacity still ${limiter.capacity}`);
}

// ── 2. Volumetric flood sheds safely ────────────────────────────────────────
function floodShedsSafely(): void {
  const clock = new ManualClock(0);
  const limiter = sketchRateLimit({ limit: 5, windowMs: 1_000, clock });

  // 1,000,000 distinct keys. A per-key limiter would allocate ~1M records.
  // The sketch holds fixed memory and sheds aggressively — always in the safe direction.
  let admitted = 0;
  for (let i = 0; i < 1_000_000; i++) {
    if (limiter.checkSync(`flood-${i}`).allowed) admitted++;
  }
  console.log(`1,000,000 keys → ${admitted} admitted; capacity still ${limiter.capacity}`);
}

// ── 3. Never over-admits a hot key ──────────────────────────────────────────
function neverOverAdmits(): void {
  const clock = new ManualClock(0);
  const limiter = sketchRateLimit({ limit: 5, windowMs: 1_000, clock });

  let allowed = 0;
  for (let i = 0; i < 20; i++) {
    if (limiter.checkSync("attacker").allowed) allowed++;
  }
  console.log(`hot key: ${allowed} allowed out of 20 (limit 5) — never over-admits:`, allowed <= 5);

  // Window roll resets the budget
  clock.advance(1_000);
  console.log("after window roll, allowed again:", limiter.checkSync("attacker").allowed);
}

exactWithinBudget();
floodShedsSafely();
neverOverAdmits();

mergeableSketch() — Distributed Use

For cluster-wide frequency estimation (e.g. detecting which source IPs are heavy hitters across a fleet), each node keeps its own mergeableSketch and periodically shares its state with peers. Because CMS counters are purely additive, summing node sketches yields the union — a global frequency estimate in the same fixed footprint.
import { mergeableSketch, sketchSnapshotFromBytes } from "throttlekit";

// Each node maintains its own sketch
const local = mergeableSketch({
  epsilon: 0.01,
  delta: 0.001,
  // All nodes in the cluster MUST use the same seed for merges to be meaningful
  seed: 0x9e3779b1, // the default fixed seed — peers merge out of the box
});

// Track local traffic
local.add("192.0.2.1");
local.add("192.0.2.1");
local.add("203.0.113.5");

// Ship to a coordinator (any transport you own: gossip, HTTP, gRPC)
const bytes = local.toBytes();
// ...on the coordinator:
const snap  = sketchSnapshotFromBytes(bytes);
coordinator.merge(snap);

// Query the global estimate
console.log("global count for 192.0.2.1:", coordinator.estimate("192.0.2.1"));

MergeableSketchOptions

epsilon
number
Additive accuracy. Default 0.01.
delta
number
Failure probability. Default 0.001.
seed
number
Hash seed. All merging nodes must use the same seed. Defaults to a fixed shared constant (0x9e3779b1) so peers merge out of the box.
mergeableSketch is an eventually-consistent estimator, not a strongly-consistent global limiter. It is designed for best-effort detection and shedding. For an exact hard limit across a fleet, use rateLimit over a Redis/Postgres store, or twoTier in leased mode.

sketchSnapshotFromBytes(bytes)

Decodes bytes produced by MergeableSketch.toBytes() back into a SketchSnapshot for merging. Validates that total is finite and non-negative; a poisoned peer total would corrupt the cluster-wide N in the epsilon × N error bound and is rejected.
The Count-Min Sketch bound is an additive approximation: the overcount is at most epsilon × N where N is the total number of items added to the sketch across all keys in the current window. If one window sees 10,000 total requests and epsilon = 0.01, the worst-case overcount for any single key is 100. This means with heavy total traffic, a key that actually had 0 requests might appear to have up to 100 — causing early denial. Shrinking epsilon tightens this, at the cost of more memory.
An attacker who knows the sketch’s hash seed can precompute a set of keys that all hash to the same counter positions. If those counter positions belong to a target victim key, the attacker can inflate the victim’s estimate and cause it to be falsely denied (a griefing attack). A random per-instance seed makes this precomputation impossible: the attacker cannot know the seed without observing the running process. Only pass a fixed seed in tests or when you are certain the endpoint is not publicly reachable.

Build docs developers (and LLMs) love