@unikvs/rate-limit
Middleware that limits operation frequency with a token bucket.
Overview
RateLimit limits operation frequency with a token bucket, protecting storage from excessive reads and writes.Middleware
Use it to allow bursts while capping the average rate. On exhaustion, choose waiting or an immediate error.
nameisRateLimit.isOpenis alwaystrue.- The bucket is shared per middleware instance (not per key), consuming 1 token for every operation (
set/get/stream/has/delete/copy/move/clear). encode/decode/encodeStream/decodeStreampass data through.
Install with:
npm install @unikvs/rate-limitpnpm add @unikvs/rate-limityarn add @unikvs/rate-limitbun add @unikvs/rate-limitnub add @unikvs/rate-limitaube add @unikvs/rate-limitUsage
RateLimit implements IMiddleware. Pass it to UniKvs with use().
import { RateLimit } from "@unikvs/rate-limit";
Constructor arguments:
tokensPerInterval?number
Tokens refilled per interval.
number1intervalMs?number
Refill interval (ms).
number1000maxTokens?number
Maximum tokens held. Defaults to max(1, tokensPerInterval). An explicit value below 1 fails construction.
numberburst?number
Alias of maxTokens. When both are given, maxTokens wins. An explicit value below 1 fails construction.
numberstrategy?RateLimitStrategy
Behavior on exhaustion. "wait" waits for refill; "throw" throws immediately.
RateLimitStrategy"wait"now?() => number
Returns the current time (epoch ms). Defaults to Date.now.
() => numberDate.nowsleep?IRateLimitSleep
Wait implementation. Uses setTimeout by default; swap it to control time in tests.
IRateLimitSleepThe following caps usage at 5 operations per second, waiting on exhaustion:
import { Memory } from "@unikvs/memory";
import { RateLimit } from "@unikvs/rate-limit";
import * as unikvs from "unikvs";
const kvs = new unikvs.Builder<{ k1: unikvs.PlainValue<string> }>()
.use(new RateLimit({ tokensPerInterval: 5, intervalMs: 1000 }))
.persist(new Memory())
.build();
await kvs.open();
await kvs.set("k1", "hello");
console.log(await kvs.get("k1")); // "hello"
await kvs.close();
RateLimit
Middleware that limits operation frequency with a token bucket. It extends PassThrough and performs no data conversion.
RateLimitOptions
Options accepted by the RateLimit constructor. Specifies token counts, refill interval, and exhaustion behavior.
RateLimitStrategy
Behavior on exhaustion. "wait" waits until tokens refill; "throw" throws immediately.
IRateLimitSleep
Wait implementation. Swap it to control time in tests.
Data types
| Method | Input | Output |
|---|---|---|
encode |
any |
any |
decode |
any |
any |
encodeStream |
any |
any |
decodeStream |
any |
any |
useconsumes one token per operation and never touches data.encode/decode/encodeStream/decodeStreampass input through unchanged.- Tokens refill with elapsed time and never exceed the cap. A clock moving backward never adds tokens.
- When
maxTokensis omitted, the capacity ismax(1, tokensPerInterval). Since each operation consumes one token, atokensPerIntervalbelow 1 still yields a capacity of 1. - An explicit
maxTokensorburstbelow 1 throwsRateLimitInvalidOptionErrorat construction.tokensPerIntervalmay be a fractional value below 1. - The
waitstrategy waits until recovery, serving requests FIFO. It aborts waiting on the abort signal.
Errors
Errors thrown by this package:
RateLimitExceededError
retryAfterMsnumber
Time to wait before retrying (ms).
numberMeaning
Thrown when an operation arrives with no tokens left and strategy is "throw". next is not called and the operation never runs.
How to fix
Wait meta.retryAfterMs before retrying, or use strategy: "wait" for automatic waiting. If this happens often, revisit tokensPerInterval or maxTokens.
Example
Catching RateLimitExceededError:
import { RateLimitExceededError } from "@unikvs/rate-limit";
try {
await kvs.set("k1", "hello");
} catch (ex) {
if (ex instanceof RateLimitExceededError) {
console.log(ex.meta); // { retryAfterMs: 500 }
} else {
throw ex;
}
}
RateLimitInvalidOptionError
Meaning
Thrown when the constructor receives invalid options: non-positive-finite numbers, an explicit maxTokens or burst below 1, a strategy other than "wait" / "throw", or a non-function now / sleep.
How to fix
Check meta.name: pass positive finite numbers, a valid enum value, or functions. When both maxTokens and burst are given, maxTokens wins.
Example
Catching RateLimitInvalidOptionError:
import { RateLimit, RateLimitInvalidOptionError } from "@unikvs/rate-limit";
try {
const limiter = new RateLimit({ intervalMs: 0 });
} catch (ex) {
if (ex instanceof RateLimitInvalidOptionError) {
console.log(ex.meta); // { name: "intervalMs", actual: 0 }
} else {
throw ex;
}
}
AbortError from the abort signal propagates as-is, and this middleware consumes no token in that case.