express-secure-limiter
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
redisandioredis). - Multi-Algorithm Support — Switch between
fixed-window,sliding-window(default), andtoken-bucketusing config. - Brute-Force Protection — Pre-built
SignInLimiterwith custom failed attempts tracking and customizable account lockouts. - Tiered Request Limiting — Pre-built
RequestLimitersupporting dynamic limit resolution based on request context (e.g. user plans, API keys). - Email-Based OTP 2FA — Pre-built
OtpServiceusingnodemailerto 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 redisQuick 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
- Password Authentication: Client sends login request to
/loginwith credentials. - 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 }. - 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('user@example.com');
// Verify OTP
const result = await otpService.verifyOtp('user@example.com', '123456');
if (result.success) {
// Successful verification
} else {
// Handle result.error ('RateLimitExceeded', 'InvalidOtp', 'ExpiredOtp', etc.)
}Gmail App Password Setup
- Enable 2-Step Verification on your Gmail account.
- Visit Google App Passwords.
- Generate a new App Password (e.g. for "Mail" / "Other").
- Copy the generated 16-character code and set it as
GMAIL_APP_PASSWORDin your environment. - Set
GMAIL_USERto your Gmail address.
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