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

limitkit

v0.2.0

Published

Rate limiting as a decision, not a middleware: fixed and sliding windows over an injectable store, a bounded in-memory store, standard RateLimit headers, and client-IP extraction. No HTTP client, no framework.

Downloads

508

Readme

limitkit

Rate limiting as a decision, not a middleware.

pnpm add github:bitbaum/limitkit#v0.2.0

Why this exists

Twelve near-identical rate limiters were found across nine repos of one fleet — orangecat alone carried four, and its ADR to unify them sat "Proposed" for seven months while the count doubled. What actually varied between the twelve was one thing: where the counts live. Everything else — the window arithmetic, the refusal shape, the headers, the client-IP dance — was the same idea written twelve slightly different ways, with independently re-invented bugs (one kept an unbounded Map keyed by stranger IPs: a slow memory leak fed by the public internet).

So the algorithm is fixed and the store is the seam.

Use

import { slidingWindow, clientIp, toHeaders } from 'limitkit';

const loginLimiter = slidingWindow({ limit: 5, windowMs: 15 * 60_000 });

export async function POST(req: Request) {
  const result = loginLimiter.check(`login:${clientIp(req.headers)}`);
  if (!result.allowed) {
    return Response.json(
      { error: 'Too many attempts' },
      { status: 429, headers: toHeaders(result) },
    );
  }
  // ... the actual work, with toHeaders(result) on the success path too if you like
}
  • slidingWindow(rule, store?) — N hits per trailing window. The default; it is what most hand-rolled "fixed" windows actually meant.
  • fixedWindow(rule, store?) — N hits per aligned bucket. Cheaper, burstier at the boundary (up to 2N around a roll). Choose it by name, not by accident.
  • check counts the hit when allowed and counts nothing when refused — a hammered key recovers the moment the attacker stops, instead of locking the legitimate user behind the same NAT out forever.
  • The refusal's retryAfterSeconds is derived from the actual oldest hit, never fabricated. "Try again shortly" on a window that opens in an hour is a lie; this package refuses to tell it.

The store seam

interface Store {
  get(key: string): { hits: number[] } | undefined;
  set(key: string, state: { hits: number[] }): void;
}

The default MemoryStore is bounded (LRU past maxKeys, default 5 000) — the unbounded-Map leak is impossible by construction. It is per-process: behind N workers the effective limit multiplies by N, which is fine for blunting scripted abuse and wrong for billing enforcement. For shared state, implement those two methods over Redis/Postgres — an adapter is a dozen lines, because the store holds and the algorithm decides.

HTTP edges

  • toHeaders(result) — X-RateLimit-Limit / -Remaining / -Reset, plus Retry-After on refusals only. A plain object you spread into your own response; constructing a Response would mean choosing your framework for you.
  • clientIp(headers, { trustedProxies = 1 }) — the forwarded hop your own proxy wrote, then x-real-ip, then "unknown". A proxy appends to X-Forwarded-For, so the header reads <what the client sent>, <what the proxy saw> and only the last entry is unforgeable. Reading the first one — which this did before v0.2.0 — lets a caller vary the header per request, mint a fresh bucket each time and never trip the limit at all. Set trustedProxies: 2 when a CDN sits in front of your proxy, or 0 when the server is exposed directly and no forwarded header can be believed.
  • No HTTP client, no middleware, no framework types — the package supplies the decision; your app keeps its conventions.
  • No limit values — how many attempts a login route allows is app semantics. Centralize the rule; assert the numbers locally.
  • No distributed store — that is your Redis and your ops posture. The seam is two methods.

Everything takes now as an argument, so all of it is testable without sleeping — which is why this package has tests and the twelve files it replaces, collectively, had almost none.

Development

pnpm run verify  # lint + typecheck + build + test (tests import by package name)

MIT.