/** * Fixed-window rate limiter. * * Every window boundary resets the counter to zero. Cheap (one INCR per * request) but permits burst at boundary edges — a caller can spend the * whole budget in the last second of one window and the whole budget in * the first second of the next. Prefer sliding for user-facing APIs; fixed * is fine for "cheap-and-lax" scenarios like debounce or dedup. * * @param {import('../index.js').WindowLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function fixed(config: WindowLimiterConfig): Limiter; /** * Interpolated sliding-window rate limiter. * * Approximates a true sliding window using two fixed buckets — the current * window's counter plus a weighted slice of the previous window's counter, * where the weight is the fraction of the previous window still overlapping * with "now". This costs the same as fixed (~1 write) but eliminates the * boundary burst that plagues fixed windows. * * Accuracy: ~1% off ground truth in the worst case (Cloudflare / Kong / * Envoy all ship this variant as their default). For strict per-user API * quotas this is normally within a few requests of the true count. * * @param {import('../index.js').WindowLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function sliding(config: WindowLimiterConfig): Limiter; /** * Token-bucket rate limiter. * * A bucket of `capacity` tokens refills at `refillRate` tokens per second. * Every request consumes one token; if the bucket is empty, the request is * rejected. Allows *controlled burst* — an idle caller accumulates a full * bucket and can spend it at once, then drops to the steady rate. * * State is stored as a single JSON blob per key: `{ tokens, updatedAt }`. * Tokens are recomputed on read from the elapsed time — no background * timer needed. * * Because token accounting is more than a simple counter, this reads the * state, recomputes, and writes it back. When the store exposes the * optional atomic `compareAndSet` (the bundled memory and Redis stores * both do), the write is a CAS retried on contention — concurrent * requests can never double-spend a token. Stores without it fall back * to a last-writer-wins `set`, which can race under concurrency. * * @param {import('../index.js').BucketLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function tokenBucket(config: BucketLimiterConfig): Limiter; /** * Leaky-bucket rate limiter. * * The bucket accumulates requests up to `capacity`. Water leaks out at a * constant `leakRate` requests/second — the effective steady-state rate. * When the bucket is full, incoming requests are rejected. Unlike * token-bucket, there is **no burst tolerance**: outgoing rate is * strictly bounded by `leakRate`. * * Use for traffic shaping and protecting downstream services with a hard * throughput ceiling (SMTP relays, upstream APIs with quota-per-second). * For user-facing endpoints, prefer `sliding` or `tokenBucket`. * * State encoding matches token-bucket: `"|"`. * Writes go through the store's optional atomic `compareAndSet` when * available (bundled memory + Redis stores both provide it) so * concurrent requests can't race the level; stores without it fall * back to last-writer-wins `set`. * * @param {import('../index.js').BucketLimiterConfig} config * @returns {import('../index.js').Limiter} */ declare function leakyBucket(config: BucketLimiterConfig): Limiter; /** * Combine multiple limiters into one. A request is allowed only if **every** * inner limiter allows it; when any denies, the request is rejected with * the strictest `retryAfter` (max over deniers). * * Use for API-key style layered quotas — e.g. 100/min AND 1k/hour AND 10k/day. * Each inner limiter is a fully-formed limiter object; they can differ in * algorithm, window, or even backing store. * * @param {{ limiters: Array }} config * @returns {import('./index.js').Limiter} */ declare function multi(config: { limiters: Array; }): Limiter; /** * Wrap any limiter with a violation-count ban policy. When a caller * triggers `threshold` denials within `trackingWindow`, we bump them to * a hard ban for `banDuration` — subsequent `.check()` calls short-circuit * to denied without touching the base limiter. * * Sits nicely on top of a stricter-than-you-need limiter: the base * limiter catches ordinary abuse, `withBan` catches persistent abuse * cheaply (a single `store.get` per request while in the ban window, * no HMAC / no algorithm work). * * const limiter = rateLimit.withBan( * rateLimit.sliding({ requests: 20, window: '1m', store }), * { store, threshold: 5, banDuration: '1h' }, * ) * * State is written with the prefixes `bs:v:` (violation counter, * TTL = trackingWindow) and `bs:b:` (ban marker, TTL = banDuration). * You can share the base limiter's store or provide a dedicated one. * * @param {import('./index.js').Limiter} limiter * @param {{ * store: import('./index.js').RateLimitStore, * threshold: number, * banDuration: string | number, * trackingWindow?: string | number, * }} options * @returns {import('./index.js').Limiter} */ declare function withBan(limiter: Limiter, options: { store: RateLimitStore; threshold: number; banDuration: string | number; trackingWindow?: string | number; }): Limiter; declare function memoryStore(options?: {}): { get(key: any): Promise<{ count: number; expiresAt: number; } | null>; read(key: any): Promise<{ count: number; expiresAt: number; } | null>; incr(key: any, ttlMs: any): Promise<{ count: number; expiresAt: any; }>; set(key: any, count: any, ttlMs: any): Promise; decr(key: any): Promise; compareAndSet(key: any, expected: any, value: any, ttlMs: any): Promise; delete(key: any): Promise; reset(key: any): Promise; _size: () => number; _stop: () => void; }; /** * Wrap a user-supplied store into the interface the rate-limit algorithms * expect. Validates that the required methods exist so misconfigurations * surface at startup, not at the first blocked request. * * Required methods (all may be async): * - `get(key)` → { count, expiresAt } | null * - `incr(key, ttlMs)` → { count, expiresAt } (atomic) * - `set(key, count, ttlMs)` → void * - `delete(key)` → void * * Optional: * - `reset(key)` → void (defaults to `delete`) * - `decr(key)` → void — atomic decrement of an existing * key. When provided, `sliding` uses it for race-free rollback of a * rejected request's tentative increment. * - `compareAndSet(key, expected, value, ttlMs)` → boolean — atomic CAS * (`expected: null` = key must not exist). When provided, * `tokenBucket` / `leakyBucket` become race-free under concurrency. * * Atomicity guarantee: `incr` must be atomic across concurrent callers. On * Redis, wrap `INCR + EXPIRE` in a Lua script or MULTI/EXEC. On Mongo, use * `findOneAndUpdate` with upsert. Non-atomic implementations race and let * requests bypass the limit under load. * * @param {Partial} impl * @returns {import('../index.js').RateLimitStore} */ declare function customStore(impl: Partial): RateLimitStore; /** * Redis-compatible store. Works with any client that exposes: * - `eval(script, numkeys, ...args)` → number | string * - `pttl(key)` → number (ms; -2 missing, -1 no ttl) * - `get(key)` → string | null * - `set(key, value, 'PX', ms)` → 'OK' * - `del(key)` → number * * Verified compat: `ioredis`, `node-redis` (v4+), `@upstash/redis` (HTTP, * works on Cloudflare Workers / Vercel Edge / Deno Deploy). * * Atomicity: * - `incr` runs a single Lua script that INCR's the key and PEXPIRE's it * only when the key is fresh (count == 1). TTL anchors to the first * increment in the window — correct for fixed / token-bucket. * - `read` runs a Lua script (GET + PTTL) so the snapshot is consistent * even under contention. * * Optimization: when the client is `ioredis` (detected via `defineCommand`), * both scripts are registered once as named commands. Subsequent calls go * out as `EVALSHA`, saving the script body on every request. * * Namespacing: every key is prefixed with `rl:` by default so this adapter * doesn't collide with your application keys. Configurable via `prefix`. * * @param {object} client Redis-compatible client instance. * @param {{ prefix?: string }} [options] * @returns {import('../index.js').RateLimitStore} */ declare function redisStore(client: object, options?: { prefix?: string; }): RateLimitStore; declare namespace rateLimit { export { fixed }; export { sliding }; export { tokenBucket }; export { leakyBucket }; export { multi }; export { withBan }; export namespace stores { export { memoryStore as memory }; export { customStore as custom }; export { redisStore as redis }; } } /** * A store entry snapshot returned by `get` / `read` / `incr`. */ type StoreEntry = { /** * Current counter value for the key. */ count: number; /** * Absolute unix-ms timestamp when the key * stops being valid. */ expiresAt: number; }; /** * The store contract every rate-limit backend must satisfy. * * All methods are async. `incr` must be atomic across concurrent callers — * the algorithm layer relies on that guarantee. */ type RateLimitStore = { /** * Fetch current state. May refresh LRU position on stores that maintain one. */ get: (key: string) => Promise; /** * Non-mutating snapshot — does NOT refresh LRU / activity state. */ read: (key: string) => Promise; /** * Atomically increment (or create) the key and return the new state. */ incr: (key: string, ttlMs: number) => Promise; /** * Overwrite (or create) the key with an explicit count and TTL. */ set: (key: string, count: number, ttlMs: number) => Promise; delete: (key: string) => Promise; reset: (key: string) => Promise; /** * Optional: atomically decrement an existing key (no-op when absent; * never creates the key). `sliding` uses this to roll back its * tentative increment on rejection without racing concurrent `incr`s — * stores that omit it fall back to a read-modify-write rollback. */ decr?: ((key: string) => Promise) | undefined; /** * Optional: atomically write `value` only if the key's current stored * count equals `expected` (`null` = key must not exist). Returns * `true` when the write happened. `tokenBucket` / `leakyBucket` use * this as a CAS so concurrent requests can't double-spend a token — * stores that omit it fall back to last-writer-wins `set`. */ compareAndSet?: ((key: string, expected: string | null, value: string | number, ttlMs: number) => Promise) | undefined; }; type CheckInput = { key: string; }; type LimiterResult = { allowed: boolean; remaining: number; reset: Date | null; retryAfter: number | null; }; /** * A composable limiter. Every algorithm and `multi()` returns this shape. */ type Limiter = { check: (input: CheckInput) => Promise; }; type WindowLimiterConfig = { requests: number; /** * Duration string ('1m', '30s') or ms. */ window: string | number; store: RateLimitStore; }; type BucketLimiterConfig = { capacity: number; /** * Token-bucket only: tokens/second refill. */ refillRate?: number | undefined; /** * Leaky-bucket only: leak rate in req/sec. */ leakRate?: number | undefined; store: RateLimitStore; }; export { customStore, fixed, leakyBucket, memoryStore, multi, rateLimit, redisStore, sliding, tokenBucket, withBan }; export type { BucketLimiterConfig, CheckInput, Limiter, LimiterResult, RateLimitStore, StoreEntry, WindowLimiterConfig };