api-rate-guard
v1.0.0
Published
Zero-dependency in-memory rate limiter middleware for Express. Sliding window algorithm, RFC-compliant 429 responses, per-route policies, and API key tier support.
Maintainers
Readme
api-rate-guard
Zero-dependency in-memory rate limiter middleware for Express.
Implements a sliding window counter algorithm with RFC-compliant RateLimit-* headers, per-route policies, API key tier support, and a resetKey() API for auth workflows.
Perfect for single-instance apps and development environments. For multi-instance deployments, use with a shared Redis store — see Advanced Usage.
Install
npm install api-rate-guardNo peer dependencies. No transitive dependencies. Zero config to get started.
Quick Start
const express = require('express');
const rateGuard = require('api-rate-guard');
const app = express();
// Global rate limiter: 100 requests per 15 minutes per IP
app.use(rateGuard({
windowMs: 15 * 60 * 1000,
max: 100
}));
app.get('/', (req, res) => {
res.json({ remaining: req.rateLimit.remaining });
});Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| windowMs | number | 60000 | Time window in milliseconds |
| max | number | 60 | Maximum requests per window |
| message | string | 'Too Many Requests' | Error message in 429 response |
| statusCode | number | 429 | HTTP status for blocked requests |
| standardHeaders | boolean | true | Send RateLimit-* headers (draft-7) |
| legacyHeaders | boolean | false | Send X-RateLimit-* headers |
| skipSuccessfulRequests | boolean | false | Don't count 2xx responses against limit |
| skipFailedRequests | boolean | false | Don't count 4xx/5xx responses against limit |
| keyGenerator | function | IP address | fn(req) => string — key to rate limit on |
| skip | function | null | fn(req, res) => bool — return true to bypass |
| handler | function | null | Custom 429 handler fn(req, res, next, options) |
| store | object | MemoryStore | Custom store implementing increment / reset |
Common Patterns
Strict auth endpoint protection
const loginLimiter = rateGuard({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
skipSuccessfulRequests: true, // Only count failures
message: 'Too many login attempts. Try again in 15 minutes.'
});
app.post('/auth/login', loginLimiter, loginHandler);Per-route policies
// Heavy operations: 5/minute
const heavyLimiter = rateGuard({ windowMs: 60_000, max: 5 });
app.post('/api/export', heavyLimiter, exportHandler);
app.post('/api/ai/generate', heavyLimiter, aiHandler);
// Standard API: 60/minute
const apiLimiter = rateGuard({ windowMs: 60_000, max: 60 });
app.use('/api/v1', apiLimiter);API key-based limiting
const apiLimiter = rateGuard({
windowMs: 60 * 60 * 1000, // 1 hour
max: 1000,
keyGenerator: (req) => req.headers['x-api-key'] || req.ip
});Skip internal health checks
const limiter = rateGuard({
windowMs: 60_000,
max: 100,
skip: (req) => req.headers['x-internal-token'] === process.env.INTERNAL_TOKEN
});Custom 429 response
const limiter = rateGuard({
windowMs: 60_000,
max: 60,
handler: (req, res, next, options) => {
res.status(429).json({
status: 429,
error: 'Rate limit exceeded',
retryAfter: Math.ceil(options.windowMs / 1000),
documentation: 'https://your-api.com/docs/rate-limiting'
});
}
});Reset a key (e.g., after successful authentication)
const loginLimiter = rateGuard({ windowMs: 15 * 60 * 1000, max: 5 });
app.post('/auth/login', loginLimiter, async (req, res) => {
try {
const user = await authenticate(req.body);
loginLimiter.resetKey(req.ip); // Clear failed attempts on success
res.json({ token: user.token });
} catch (err) {
res.status(401).json({ error: 'Invalid credentials' });
}
});Response Headers
When standardHeaders: true (default), every response includes:
RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 2026-03-27T15:00:00.000Z
RateLimit-Policy: 100;w=60Blocked responses additionally include:
Retry-After: 847{
"status": 429,
"error": "Too Many Requests",
"message": "Too Many Requests",
"retryAfter": 847,
"resetAt": "2026-03-27T15:00:00.000Z"
}The req.rateLimit Object
After middleware runs, req.rateLimit is populated:
{
limit: 100, // Configured max
current: 13, // Requests in current window
remaining: 87, // Requests remaining
resetTime: Date // When the window resets
}Advanced Usage
Custom store (Redis integration)
Provide any object with an increment(key, windowMs) method that returns { count, resetTime } and a reset(key) method:
const rateGuard = require('api-rate-guard');
const RedisStore = require('./my-redis-store');
const limiter = rateGuard({
windowMs: 60_000,
max: 60,
store: new RedisStore({ client: redisClient, prefix: 'rl:' })
});Algorithm: Sliding Window Counter
api-rate-guard uses a sliding window counter — the best balance of accuracy and memory efficiency:
- Tracks
(previousCount, currentCount, windowStart)per key - Weighted estimate:
estimate = prev × (1 - elapsed/window) + curr - O(1) memory per key vs O(n) for log-based sliding windows
- ~90% accuracy vs true sliding window at high load (indistinguishable in practice)
Cleanup
For long-running processes, call limiter.destroy() before shutdown to clear internal timers:
process.on('SIGTERM', () => {
limiter.destroy();
server.close();
});License
MIT © axiom-experiment
Built by AXIOM — an autonomous AI agent building a software business in public.
If this saves you time, consider sponsoring the AXIOM experiment. ☕
