A zero-dependency, in-memory sliding-window rate limiter for Node. One small file, fully typed, self-pruning so it won't leak memory. Perfect for protecting an API route, a queue worker, or a CLI on a single instance — no Redis required.
- 🪶 Zero dependencies, tiny footprint
- 🎚️ True sliding window (not a coarse fixed bucket)
- 🧹 Self-pruning — an
unref'd timer sweeps stale keys; never keeps your process alive - 🧰 Class API (
peek/reset/dispose) and a one-liner - 📦 Ships ESM + CJS + types
Single-process by design. Behind a load balancer each instance limits independently — fine for coarse abuse protection; use a shared store (Redis) when you need a strict global limit.
npm install @oratis/rate-limitimport { rateLimit } from "@oratis/rate-limit";
// Allow 100 requests per minute per IP.
const { allowed, remaining, resetMs } = rateLimit(ip, 100, 60_000);
if (!allowed) {
return new Response("Too Many Requests", {
status: 429,
headers: { "Retry-After": String(Math.ceil(resetMs / 1000)) },
});
}Reach for RateLimiter when you want peek, reset, or explicit lifecycle
control:
import { RateLimiter } from "@oratis/rate-limit";
const limiter = new RateLimiter({ limit: 5, windowMs: 10_000 });
limiter.check("user:42"); // record a request → RateLimitResult
limiter.peek("user:42"); // inspect without recording
limiter.reset("user:42"); // forget one key
limiter.reset(); // forget everything
limiter.dispose(); // stop the prune timer when you're done| Field | Type | Meaning |
|---|---|---|
allowed |
boolean |
Whether the request is within the limit. |
remaining |
number |
Requests left in the current window (0 when blocked). |
resetMs |
number |
Ms until capacity frees up (great for a Retry-After header). |
| Option | Type | Default | Description |
|---|---|---|---|
limit |
number |
— | Max requests per rolling window (> 0). |
windowMs |
number |
— | Window length in ms (> 0). |
pruneIntervalMs |
number |
300000 |
Stale-key sweep interval. 0 disables the timer. |
Each key keeps the timestamps of its requests. On every check, timestamps
older than windowMs are dropped; if what remains is below limit, the request
is allowed and recorded. This gives a true rolling window — no burst at the
fixed-bucket boundary that naive counters suffer from.