npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@youneed/server-middleware-rate-limit

v0.2.0

Published

@youneed/server middleware: rate limiting with pluggable strategies (fixed/sliding/token-bucket/leaky-bucket/exponential + KV).

Downloads

236

Readme

@youneed/server-middleware-rate-limit

Rate-limit requests with a pluggable strategy (default 429 Too Many Requests), emitting standard X-RateLimit-* and Retry-After headers. Pick a strategy with a factory function — or pass your own RateLimiter instance.

import { Application } from "@youneed/server";
import { rateLimit, fixedWindow, tokenBucket } from "@youneed/server-middleware-rate-limit";

Application()
  .use(rateLimit({ strategy: fixedWindow({ windowMs: 60_000, max: 100 }) }))  // global
  .use("/api", rateLimit({ strategy: tokenBucket({ capacity: 50, refillPerSec: 5 }) }))
  .listen(3000, () => {});

| factory | shorthand | behaviour | | --- | --- | --- | | fixedWindow(opts?) | "fixed" | one counter per windowMs; cheapest, can allow up to 2×max across a boundary | | slidingWindow(opts?) | "sliding" | limit holds over the last windowMs at every instant; no boundary burst | | tokenBucket(opts?) | "token-bucket" | spend one token per request, refill continuously; allows bursts up to capacity | | leakyBucket(opts?) | "leaky-bucket" | requests pour in, the bucket drains at leakPerSec; burst of capacity, then a strict one-per-interval pace (GCRA, Nginx limit_req model) | | exponentialBackoff(opts?) | "exponential" | cooldown DOUBLES each strike (capped at maxBlockMs); a clean window forgives | | kvFixedWindow(kv, opts?) | — | distributed fixed window on a shared KV — holds across instances (below) |

Factories return the strategy classes (FixedWindow, SlidingWindowLog, TokenBucket, LeakyBucket, ExponentialBackoff, KvFixedWindow) — exported too, for subclassing. String shorthands (strategy: "fixed", configured via the top-level windowMs/max/maxBlockMs) keep working for quick configs.

Deep imports

Every strategy is also importable from its own subpath — handy when you want just the limiter (e.g. in a test harness) without pulling the whole module:

import { fixedWindow } from "@youneed/server-middleware-rate-limit/strategies/fixedWindow.js";
import { leakyBucket } from "@youneed/server-middleware-rate-limit/strategies/leakyBucket.js";

Application().use("/api", rateLimit({ strategy: fixedWindow({ max: 100 }) }));

Available subpaths: strategies/fixedWindow.js, strategies/slidingWindow.js, strategies/tokenBucket.js, strategies/leakyBucket.js, strategies/exponentialBackoff.js, strategies/kvFixedWindow.js — each exports the class, the factory and the config type.

Default key is the client IP. Override with key: (ctx) => … (e.g. an API key or user id). Drop in a custom limiter by subclassing RateLimitStrategy.

Provider form: the controller drives the limiter

rateLimitProvider() is a ControllerProvider injecting this.rateLimit — for per-endpoint limits, conditional limiting (only the expensive paths), or several checks per request, without mounting middleware on the route:

import { Controller, Response } from "@youneed/server";
import { rateLimitProvider, tokenBucket } from "@youneed/server-middleware-rate-limit";

class Billing extends Controller("/billing", {
  providers: [rateLimitProvider({ strategy: tokenBucket({ capacity: 10, refillPerSec: 1 }) })],
}) {
  @Controller.post("/charge")
  async charge() {
    await this.rateLimit.enforce(); // 429 + Retry-After when over — same as the middleware
    return Response.json({ ok: true });
  }

  @Controller.get("/quota")
  async quota() {
    const d = await this.rateLimit.check(); // verdict only — you decide what it means
    return Response.json({ limited: d.limited, remaining: d.remaining });
  }
}
  • check(key?)Promise<RateDecision> — records a hit, sets the X-RateLimit-* headers on the current response, returns the verdict (limited/remaining/resetMs/retryAfterMs). key defaults to the client key of the ambient request (same resolution as the middleware).
  • enforce(key?)check + the standard rejection when limited: Retry-After + HttpError(429) (status/message overridable via options).
  • Options: everything rateLimit() takes, plus member — the instance member name (default "rateLimit").

Distributed limits across instances (KV-backed)

The built-in strategies above keep their counters in an in-process Map. That is correct for a single instance, but behind a load balancer with N app instances each process counts on its own — so the effective limit becomes max × N. To enforce one shared limit you need a shared counter.

kvFixedWindow(kv) is a fixed-window limiter backed by a @youneed/kv store. Every instance increments the same bucket key in the shared store, so the limit holds across the whole fleet:

import { Application } from "@youneed/server";
import { rateLimit, kvFixedWindow } from "@youneed/server-middleware-rate-limit";
import { RedisKV } from "@youneed/kv-redis"; // a store shared by every instance

const kv = new RedisKV({ url: process.env.REDIS_URL });

Application()
  .use(rateLimit({ strategy: kvFixedWindow(kv, { windowMs: 60_000, max: 100 }) }))
  .listen(3000, () => {});

| option | default | meaning | | --- | --- | --- | | windowMs | 60_000 | fixed window length | | max | 100 | requests allowed per window | | prefix | "rl:" | key prefix in the store (lets several limiters share one KV) |

How it stays correct under concurrency: each request does a single kv.incr(key, { ttl }), which atomically increments the per-window bucket counter and — only when that bucket key is first created — sets its expiry. One atomic op means no read-modify-write race between instances; the verdict (count > max) is computed from the returned value.

The shared store is what makes the limit correct. Point a MemoryKV at it and you are back to per-process counting (MemoryKV lives in one process). Use a shared adapter such as @youneed/kv-redis in production so every instance reads and writes the same counter.

Because KvFixedWindow.check is async, rateLimit() awaits the verdict. The limiter contract is the exported RateLimiter interface ({ limit; check(key, now): RateDecision | Promise<RateDecision> }); the abstract RateLimitStrategy and all built-ins implement it, and you can pass any RateLimiter instance as strategy.