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

easy-rate-limit

v1.0.0

Published

Simple Express rate-limiting middleware with fixed-window counter

Readme

easy-rate-limit

Simple, tiny Express rate-limiting middleware with zero setup.

Fixed-window counter per IP (or any key you choose). Includes an in-memory store, X-RateLimit-* headers, custom handlers, and pluggable stores for Redis etc. Written in TypeScript.

Installation

npm install easy-rate-limit

Requirements: Node >= 18, express >= 5.

Quick start

import express from "express";
import limiter from "easy-rate-limit";

const app = express();

// 100 requests per minute per IP
app.use(limiter({ max: 100 }));

app.get("/", (req, res) => res.send("Hello!"));
app.listen(3000);

That's it — over-limit requests get 429 with { error, retryAfter }.

Features

  • 🚀 One-line setup, sensible defaults
  • 🔑 Key by IP, user id, API key — sync or async
  • ⏭️ skip() health checks or allowlists
  • 📦 X-RateLimit-Limit / Remaining / Reset + Retry-After headers
  • 🧩 Custom handler and onLimitReached hooks
  • 💾 Pluggable Store (Redis-ready), MemoryStore built in
  • 📘 Full TypeScript types included

Why choose easy-rate-limit?

| If you want… | easy-rate-limit | express-rate-limit | | ----------------------------------------- | ------------ | -------------------- | | One-line setup, zero config | ✅ | ✅ | | Tiny API you can read in 5 minutes | ✅ | ❌ (many options, plugins) | | Async keyGenerator / skip / handler | ✅ | ⚠️ (mostly sync) | | Fail-fast on bad config (max, windowMs, store) | ✅ | ⚠️ (some cases silent) | | Store errors never crash your app (next(err)) | ✅ | ✅ | | Timer unref'd so it never keeps Node alive | ✅ | ✅ | | Redis store included (RedisStore) | ✅ | ✅ (via plugin) | | MongoDB store included (MongoStore) | ✅ | ❌ | | Distributed / multi-instance ready | ✅ (Redis/Mongo) | ✅ |

Pick easy-rate-limit when: you want the smallest thing that works — a side project, internal tool, or single-instance API where app.use(limiter({ max: 100 })) is all you need. Scale to Redis/Mongo later without changing your middleware code.

Pick express-rate-limit when: you need its larger ecosystem (Memcached/Prisma/Cloudflare adapters, sliding windows).

How much traffic can it handle?

| Setup | Good for | Notes | | ----- | -------- | ----- | | MemoryStore (default) | ~1k–10k req/s on one Node instance; small–medium single-instance apps | Counters live in process memory: lost on restart, not shared between instances. Each key is one Map entry, so size it by unique clients × windows. | | RedisStore | Horizontal scale: add app instances freely; Redis handles 50k–100k ops/s on modest hardware | All instances share counters. Needs a reachable Redis. Increment is atomic (INCR); one extra PTTL read per request. | | MongoStore | Medium multi-instance apps already running MongoDB | One findOne + one updateOne per request; create the resetAt index (done automatically unless ensureIndex: false). Slower than Redis under very high load. |

Rule of thumb: start with MemoryStore for one server; switch to RedisStore the moment you run 2+ instances or need counters to survive restarts. The middleware code doesn't change — only the store option.

Usage

Basic

import limiter from "easy-rate-limit";

app.use(
  limiter({
    windowMs: 60_000, // 1 minute
    max: 100, // 100 requests per window per key
  }),
);

Login route (strict limit + manual reset)

import { ApiLimiter } from "easy-rate-limit";

const loginLimiter = new ApiLimiter({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: "Too many login attempts, try again later.",
});

app.use("/login", loginLimiter.middleware());

// e.g. after successful login, or in an admin endpoint:
await loginLimiter.reset("1.2.3.4"); // one client
await loginLimiter.reset(); // everyone

Per-user keys, skip health checks, custom response

app.use(
  limiter({
    windowMs: 60_000,
    max: 10,
    keyGenerator: (req) => req.headers["x-user-id"] as string,
    skip: (req) => req.path === "/health",
    handler: (req, res) => {
      res.status(429).json({ custom: true });
    },
    onLimitReached: (req, key) => console.warn(`rate limited: ${key}`),
  }),
);

Options

| Option | Type | Default | | ---------------- | -------------------------------------- | ---------------------------------------------- | | windowMs | number (positive) | 60000 | | max | number (positive integer) | 100 | | message | string | "Too many requests, please try again later." | | statusCode | number (400–599) | 429 | | headers | boolean | true | | keyGenerator | (req) => string \| Promise<string> | req.ip ?? socket.remoteAddress ?? "unknown" | | skip | (req) => boolean \| Promise<boolean> | () => false | | handler | (req, res, next) => void \| Promise<void> | 429 JSON { error, retryAfter } | | store | Store | MemoryStore | | onLimitReached | (req, key) => void \| Promise<void> | undefined |

Invalid windowMs, max, statusCode, or store throw at construction time, so misconfigurations fail fast instead of silently misbehaving.

Response headers

When headers: true (default), every counted response includes:

  • X-RateLimit-Limit — the max for the window
  • X-RateLimit-Remaining — requests left in this window
  • X-RateLimit-Reset — window reset time (unix seconds)

Blocked responses also include Retry-After (seconds until retry).

limiter({ max: 10, headers: false }); // disable X-RateLimit-* headers

Distributed stores (Redis / MongoDB)

Point every instance at the same Redis or MongoDB and counters are shared:

import { createClient } from "redis";
import limiter, { RedisStore } from "easy-rate-limit";

const redis = createClient({ url: "redis://localhost:6379" });
await redis.connect(); // works with node-redis or ioredis clients

app.use(limiter({ max: 100, store: new RedisStore(redis) }));
import { MongoClient } from "mongodb";
import limiter, { MongoStore } from "easy-rate-limit";

const col = new MongoClient(process.env.MONGO_URL!)
  .db("app")
  .collection("rate_limits");

app.use(limiter({ max: 100, store: new MongoStore(col) }));

Notes:

  • redis / mongodb are not hard dependencies — bring the client you already use; the stores accept your connected client/collection.
  • Neither store closes your connection: shutdown() is a no-op on purpose.
  • RedisStore keys are prefixed (easy-rate-limit: by default, customizable); reset() without a key only touches that prefix.
  • MongoStore creates a resetAt index automatically (opt out with new MongoStore(col, { ensureIndex: false })).

Custom store

Anything implementing Store works:

import type { Store, HitResult } from "easy-rate-limit";

class MyStore implements Store {
  async increment(key: string, windowMs: number): Promise<HitResult> { /* ... */ }
  async get(key: string): Promise<HitResult | null> { /* ... */ }
  async reset(key?: string): Promise<void> { /* ... */ }
}

limiter({ store: new MyStore() });

API reference

| Export | Description | | ------------------ | ---------------------------------------------------------------- | | limiter(options?) (default) | Returns an Express middleware directly | | ApiLimiter | Class — .middleware(), .reset(key?), .options | | MemoryStore | Single-instance store — .increment(), .get(), .reset(), .shutdown() | | RedisStore | Distributed store for node-redis/ioredis clients (prefix option) | | MongoStore | Distributed store for MongoDB collections (ensureIndex option) | | LimiterError, InvalidStoreError | Error classes | | Types | LimiterOptions, Store, HitResult |

import limiter, { ApiLimiter, MemoryStore, RedisStore, MongoStore } from "easy-rate-limit";
import type { LimiterOptions, Store, HitResult } from "easy-rate-limit";

Note on req.ip: behind a proxy, set app.set("trust proxy", 1) so Express populates req.ip correctly.

Development

npm run build   # compile src/ → dist/
npm test        # jest suite in tests/

License

MIT — see LICENSE.