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

express-quota-limiter

v1.0.2

Published

A flexible, driver-based quota limiter middleware for Express.js

Readme

express-quota-limiter

A flexible, driver-based quota limiter middleware for Express.js, written in TypeScript.

Unlike traditional rate limiters that operate per-second or per-minute, express-quota-limiter is designed for long-window quotas (e.g. 500 emails/month, 1000 API calls/day). The storage backend is fully swappable — ship with Redis in production, and use the built-in in-memory driver for tests.

Features

  • 🔌 Driver-based — Redis, in-memory, or bring your own
  • 🔒 Atomic operations — no race conditions (Redis pipeline / NX)
  • 🏷 Custom keys — per-tenant, per-user, per-route, or any combination
  • ⚖️ Request weighting — charge different amounts per request (e.g. bulk actions)
  • Manual increment API — increment quota usage programmatically
  • 📦 Zero mandatory dependencies — storage clients are peer-injected
  • 🧪 Testable — built-in MemoryDriver needs no infrastructure
  • 💬 TypeScript-first — full types, no @types/ package needed
  • 🚦 Fail-open by default — storage outages don't block your users
  • 📡 Callbacks — hook into every check or just blocked requests

Installation

npm install express-quota-limiter
yarn add express-quota-limiter

Peer dependencies

# If using Redis (ioredis or node-redis v4)
npm install ioredis
# or
npm install redis

Quick Start

import express from "express";
import { createQuotaLimiter, RedisDriver } from "express-quota-limiter";
import { redis } from "./services/redis"; // your existing client

const app = express();

const emailQuota = createQuotaLimiter({
  storage: new RedisDriver(redis),
  limit: 500,
  keyGenerator: (req) => `quota:emails:${req.tenantId}`,
  errorMessage: "Monthly email quota exceeded",
});

app.post("/send-email", emailQuota, (req, res) => {
  res.json({ ok: true });
});

Callbacks

Use callbacks to react to quota events without touching the middleware logic. Both run fire-and-forget — errors inside them are caught and logged, and never affect the HTTP response.

onQuotaChecked — fires on every request

Receives the full context after every check, whether the request was allowed or blocked. Ideal for saving the current remaining value to your DB, sending metrics to Datadog/Prometheus, or building usage dashboards.

onQuotaExceeded — fires only on blocked requests

Receives context only when a request is blocked (remaining === 0). Ideal for logging violations, sending billing alerts, or notifying the tenant.

const emailQuota = createQuotaLimiter({
  storage: new RedisDriver(redis),
  limit: 500,
  keyGenerator: (req) => `quota:emails:${req.tenantId}`,

  // runs on every request — keeps your DB in sync
  onQuotaChecked: async ({ remaining, req }) => {
    await db.query(
      "UPDATE tenants SET quota_remaining = $1 WHERE id = $2",
      [remaining, req.tenantId]
    );
  },

  // runs only when the quota is exhausted — sends an alert
  onQuotaExceeded: async ({ req }) => {
    await slack.send(`⚠️ Tenant ${req.tenantId} exhausted their email quota`);
  },
});

Callback context

| Callback | Fields available | |---|---| | onQuotaChecked | key, limit, success, remaining, req, weight | | onQuotaExceeded | key, limit, req, weight |


Response Headers

Every request (allowed or blocked) receives these headers:

| Header | Description | |---|---| | Quota-Remaining | Remaining operations in the current window | | Quota-Limit | The configured limit |

When the quota is exceeded, the middleware responds with HTTP 429:

{
  "error": "Too Many Requests",
  "message": "Monthly email quota exceeded",
  "quota": { "limit": 500, "remaining": 0 }
}

Drivers

RedisDriver

Works with both ioredis and node-redis v4. Uses a MULTI/EXEC pipeline for atomic init + decrement.

import { RedisDriver } from "express-quota-limiter";
import Redis from "ioredis";

const redis = new Redis();
const driver = new RedisDriver(redis, {
  ttlSeconds: 60 * 60 * 24 * 30, // 30 days (default)
});

MemoryDriver

In-process store. No dependencies. Not suitable for multi-process deployments.

import { MemoryDriver } from "express-quota-limiter";

const driver = new MemoryDriver({ ttlSeconds: 3600 }); // 1 hour window

Custom Driver

Implement the QuotaStorage interface to add any backend:

import { QuotaStorage, QuotaResult } from "express-quota-limiter";

class MySQLDriver implements QuotaStorage {
  async decrement(key: string, limit: number = 100): Promise<QuotaResult> {
    // your implementation
  }
}

Request Weighting

You can charge different quota amounts per request using the quotaWeight option. This is useful for endpoints where some requests are more expensive than others (e.g. sending 1 vs 100 emails).

const quota = createQuotaLimiter({
  storage: new RedisDriver(redis),
  limit: 1000,
  keyGenerator: (req) => req.userId,
  quotaWeight: (req) => req.body.count || 1, // charge by count
});

The calculated weight is passed to storage drivers and available in callbacks.

Manual Increment

You can manually increment quota usage (e.g. for compensating failed actions or admin adjustments) using the driver's increment method:

// Example: increment usage by 10 for a user
await driver.increment('quota:emails:user123', 10);

All built-in drivers support this method. The weight parameter lets you increment by any amount.


API Reference

createQuotaLimiter(options)

| Option | Type | Default | Description | |---|---|---|---| | storage | QuotaStorage | required | The storage driver to use | | keyGenerator | (req) => string | required | Returns the quota key for a request | | limit | number \| (req) => number \| Promise<number> | 100 | Max operations per window — static or per-request | | errorMessage | string | "Quota exceeded" | Message returned on 429 | | failOpen | boolean | true | Pass requests through on storage errors | | onQuotaChecked | (ctx) => void | — | Fired after every check | | onQuotaExceeded | (ctx) => void | — | Fired only when blocked |

Dynamic limits

The limit option can resolve limits dynamically per-request — ideal for multi-tenant apps with different plans:

const emailQuota = createQuotaLimiter({
  storage: new RedisDriver(redis),
  limit: async (req) => {
    const tenant = await db.query(
      "SELECT plan FROM tenants WHERE id = $1",
      [req.tenantId]
    );
    // Pro plan: 5000/month, free plan: 500/month
    return tenant.plan === "pro" ? 5000 : 500;
  },
  keyGenerator: (req) => `quota:emails:${req.tenantId}`,
});

Testing

The built-in MemoryDriver makes unit testing straightforward:

import { createQuotaLimiter, MemoryDriver } from "express-quota-limiter";

const driver = new MemoryDriver();
const middleware = createQuotaLimiter({
  storage: driver,
  limit: 5,
  keyGenerator: (req) => `test:${req.userId}`,
});

// Use driver.clear() between tests to reset state
afterEach(() => driver.clear());

License

MIT