Skip to content

Rate Limiter

@delightstack/rate-limiter is a token-bucket rate limiter implemented as a Durable Object. Pick a key (IP, user ID, API key), and the single-instance guarantee handles concurrency for you. Zero runtime dependencies — it only imports cloudflare:workers.

  • Token bucket — a bucket refills at a constant rate up to a max; requests consume tokens and are rejected when empty.
  • Per-key buckets — one DO instance manages many independent buckets (login, api, upload) so you can limit different actions separately.
  • Native DO RPCconsume(), check(), getStatus(), setOptions(), reset() called directly, no fetch handlers.
  • Header-ready statusgetStatus() returns remaining, limit, and reset_in_ms.
  • In-memory only — state isn’t persisted; limits reset if the DO is evicted (ideal for rate limiting — no storage latency, transient state is fine).
Terminal window
pnpm add @delightstack/rate-limiter

@delightstack/rate-limiter exports a single entry: RateLimiterServer.

export { RateLimiterServer } from '@delightstack/rate-limiter';
wrangler.toml
[[durable_objects.bindings]]
name = "LIMITER"
class_name = "RateLimiterServer"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["RateLimiterServer"]

Address one instance per identity with idFromName() — an IP for per-IP limiting, a user ID for per-user, or a fixed string for a global limit:

const id = env.LIMITER.idFromName(ip_address);
const limiter = env.LIMITER.get(id);
// configure the bucket for this key (once)
await limiter.setOptions('api', { limit: 100, refill_per_second: 10 });
const allowed = await limiter.consume('api', 1);
if (!allowed) {
const status = await limiter.getStatus('api');
return new Response('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': String(status.limit),
'X-RateLimit-Remaining': String(status.remaining),
'Retry-After': String(Math.ceil(status.reset_in_ms / 1000)),
},
});
}