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-secure-limiter

v1.1.0

Published

A production-ready, highly configurable, storage-agnostic rate-limiting library for Node.js and Express.

Downloads

212

Readme

express-secure-limiter

npm version npm downloads license bundle size

A production-ready, highly configurable, storage-agnostic rate-limiting library for Node.js and Express, written in TypeScript.

It supports multiple algorithms (fixed-window, sliding-window, and token-bucket), tiered pricing/plan structures, brute-force protection (login lockout), email-based One-Time Password (OTP) 2FA, and works with both in-memory and Redis stores.


Features

  • 📦 Zero-dependency by default — In-memory rate limiting with active/passive memory cleanup.
  • Atomic Redis Adapter — Fully atomic, concurrent-safe rate limiting using Redis Lua scripts (compatible with both redis and ioredis).
  • 🛠️ Multi-Algorithm Support — Switch between fixed-window, sliding-window (default), and token-bucket using config.
  • 🔒 Brute-Force Protection — Pre-built SignInLimiter with custom failed attempts tracking and customizable account lockouts.
  • 🚀 Tiered Request Limiting — Pre-built RequestLimiter supporting dynamic limit resolution based on request context (e.g. user plans, API keys).
  • ✉️ Email-Based OTP 2FA — Pre-built OtpService using nodemailer to trigger and verify single-use 6-digit OTP codes via Gmail SMTP.
  • 🔗 Express Middleware — Out-of-the-box support for Express, providing standard headers (X-RateLimit-*, Retry-After) and whitelist skipping.

Installation

npm install express-secure-limiter

(Optional) If you want to use the Redis store, ensure you have ioredis or redis installed:

npm install ioredis
# OR
npm install redis

Quick Start

1. API Request Limiting (Tier-Based)

Configure general API rate limiting using the sliding-window algorithm, with limits resolved dynamically depending on the user's plan.

import express from 'express';
import { createRequestLimiter } from 'express-secure-limiter';

const app = express();

const apiLimiter = createRequestLimiter({
  algorithm: 'sliding-window', // 'sliding-window' | 'fixed-window' | 'token-bucket'
  windowMs: 60000,             // 1 minute window
  max: (req) => {
    // Dynamic rate limits based on user plan
    const plan = req.headers['x-user-plan'] || 'free';
    if (plan === 'enterprise') return 1000;
    if (plan === 'pro') return 100;
    return 10; // Free tier
  }
});

// Apply as middleware
app.get('/api/data', apiLimiter.middleware({
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip
}), (req, res) => {
  res.json({ data: "Sensitive API response" });
});

2. Sign-in Protection (Brute-Force & Lockout)

Track failed attempts and temporarily lock out clients after consecutive login failures.

import express from 'express';
import { createSignInLimiter } from 'express-secure-limiter';

const app = express();
app.use(express.json());

const signInLimiter = createSignInLimiter({
  maxAttempts: 5,        // Allow 5 failed attempts
  windowMs: 900000,      // Track attempts within a 15-minute window
  lockoutMs: 1800000,    // Lock out client for 30 minutes on limit breach
});

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const key = email || req.ip;

  // 1. Check if they are locked out
  const status = await signInLimiter.isLocked(key);
  if (status.locked) {
    return res.status(429).json({
      error: "Too many login attempts. Account temporarily locked.",
      retryAfterSeconds: Math.ceil(status.resetMs / 1000)
    });
  }

  // 2. Perform authentication logic
  const isAuthSuccessful = authService.verify(email, password);

  if (isAuthSuccessful) {
    // Reset attempts on successful sign-in
    await signInLimiter.reset(key);
    return res.json({ token: "JWT_TOKEN" });
  }

  // 3. Record failed attempt if validation fails
  const attempt = await signInLimiter.recordFailedAttempt(key);
  if (!attempt.allowed) {
    return res.status(429).json({
      error: "Brute force protection triggered. Locked out for 30 minutes.",
      retryAfterSeconds: Math.ceil(attempt.resetMs / 1000)
    });
  }

  return res.status(401).json({
    error: "Invalid email or password.",
    remainingAttempts: attempt.remainingAttempts
  });
});

3. Email-Based OTP 2FA (via Gmail)

You can protect critical flows like login with an email-based One-Time Password (OTP) sent using Gmail SMTP.

Flow Sequence

  1. Password Authentication: Client sends login request to /login with credentials.
  2. OTP Step Trigger: If credentials are valid, the server generates a 6-digit OTP code, stores its SHA-256 hash in MemoryStore, emails the code to the user, and responds with { otpRequired: true, email }.
  3. Verification: Client submits the code to /login/verify-otp. If verification succeeds, the server issues a session token.

Usage Example

import { MemoryStore, OtpService } from 'express-secure-limiter';

const store = new MemoryStore();
const otpService = new OtpService({
  store,
  gmailUser: process.env.GMAIL_USER,
  gmailAppPassword: process.env.GMAIL_APP_PASSWORD,
  otpExpiryMs: 300000,   // 5 minutes expiry
  maxAttempts: 5,        // Max 5 verification attempts per code
  lockoutAttempts: 5,    // Lock out IP/Email after 5 consecutive failures
});

// Send OTP
await otpService.sendOtp('[email protected]');

// Verify OTP
const result = await otpService.verifyOtp('[email protected]', '123456');
if (result.success) {
  // Successful verification
} else {
  // Handle result.error ('RateLimitExceeded', 'InvalidOtp', 'ExpiredOtp', etc.)
}

Gmail App Password Setup

  1. Enable 2-Step Verification on your Gmail account.
  2. Visit Google App Passwords.
  3. Generate a new App Password (e.g. for "Mail" / "Other").
  4. Copy the generated 16-character code and set it as GMAIL_APP_PASSWORD in your environment.
  5. Set GMAIL_USER to your Gmail address.

[!WARNING] Gmail SMTP has a daily sending cap (roughly 500/day on free accounts). This is sufficient for development, testing, and low-volume staging. For production-scale workloads, we strongly recommend swapping Gmail for a transactional email provider like AWS SES, SendGrid, Resend, or Mailgun.

4. Redis Store Integration

Integrate seamlessly with Redis to manage state across multiple instances/servers.

import Redis from 'ioredis';
import { RateLimiter, RedisStore } from 'express-secure-limiter';

const redisClient = new Redis("redis://localhost:6379");

const limiter = new RateLimiter({
  windowMs: 60000,
  max: 100,
  store: new RedisStore(redisClient, { prefix: 'api-limit:' }),
});

Configuration Reference

RateLimiterOptions (Core Configuration)

| Parameter | Type | Default | Description | | :--- | :--- | :--- | :--- | | windowMs | number | Required | Time duration of the rate limit window in milliseconds. | | max | number | (key) => number | Required | Maximum number of allowed requests in the window. | | algorithm | 'fixed-window' | 'sliding-window' | 'token-bucket' | 'sliding-window' | Rate limiting algorithm to use. | | cost | number | (key) => number | 1 | Weight of each request. | | store | Store | MemoryStore | Storage adapter to use for saving rate limit counters. | | maxTokens | number | (key) => number | max | Capacity of the token bucket (Only for token-bucket). | | refillRate | number | (key) => number | maxTokens / windowMs | Refill rate in tokens per millisecond (Only for token-bucket). |

OtpServiceOptions

| Parameter | Type | Default | Description | | :--- | :--- | :--- | :--- | | store | Store | Required | The MemoryStore instance to use for temporary OTP state storage. | | transporter | any | undefined | Custom Nodemailer transporter (useful for mocking/testing). | | gmailUser | string | process.env.GMAIL_USER | Gmail username for SMTP connection. | | gmailAppPassword | string | process.env.GMAIL_APP_PASSWORD | Gmail App Password for SMTP connection. | | otpExpiryMs | number | 300000 | Expiration time of a generated code in milliseconds (5 mins). | | maxAttempts | number | 5 | Maximum verification attempts per single code before invalidating. | | lockoutAttempts | number | 5 | Lockout threshold for consecutive wrong OTP verifications. |

MiddlewareOptions (Express Factory)

| Parameter | Type | Default | Description | | :--- | :--- | :--- | :--- | | keyGenerator | (req) => string | req.ip | Resolves a unique key from the incoming Express request. | | skip | (req) => boolean | () => false | Skip rate-limiting check entirely if this returns true. | | onLimitExceeded | (req, res, next, info) => void | Sends 429 response | Custom callback handler when rate-limiting is breached. | | statusCode | number | 429 | HTTP status code for default exceeded response. | | message | any | 'Too Many Requests' | JSON response body for default exceeded response. |


License

MIT