@nextrush/rate-limit
Production-grade rate limiting with multiple algorithms and tiered limits.
Without rate limiting, a single client can exhaust your server's resources — intentionally or by accident. This middleware controls how many requests each client can make within a time window.
It supports three algorithms, tiered limits for different user types, CIDR-based whitelists and blacklists, and IETF-compliant response headers.
Default Behavior
With default options, rateLimit() applies these defaults:
| Setting | Default |
|---|---|
| Algorithm | token-bucket |
| Max requests | 100 per window |
| Window | 1m (60 seconds) |
| Key | Client IP with rl: prefix |
| Status code | 429 |
| Message | Too many requests, please try again later. |
| Standard headers | Enabled (RateLimit-*) |
| Legacy headers | Enabled (X-RateLimit-*) |
| Retry-After header | Enabled on 429 responses |
Installation
$ pnpm add @nextrush/rate-limit
Minimal Usage
import { createApp } from '@nextrush/core';
import { rateLimit } from '@nextrush/rate-limit';
const app = createApp();
// 100 requests per minute per IP, token-bucket algorithm
app.use(rateLimit());Configuration Options
app.use(
rateLimit({
max: 1000,
window: '15m',
algorithm: 'sliding-window',
trustProxy: true,
})
);RateLimitOptions
| Property | Type | Description |
|---|---|---|
algorithm | 'token-bucket' | 'sliding-window' | 'fixed-window'= 'token-bucket' | Rate limiting algorithm |
max | number= 100 | Maximum requests per window |
window | string | number= '1m' | Window duration ('1s', '1m', '1h', '1d') or milliseconds |
burstLimit? | number= max | Burst limit for token bucket algorithm |
keyGenerator? | (ctx: Context) => string | Promise<string> | Custom key generator function |
skip? | (ctx: Context) => boolean | Promise<boolean> | Skip rate limiting for matching requests |
store? | RateLimitStore= In-memory store | Custom store implementation |
handler? | (ctx: Context, info: RateLimitInfo) => void | Promise<void> | Custom handler for rate-limited requests |
onRateLimited? | (ctx: Context, info: RateLimitInfo) => void | Promise<void> | Callback when a request is rate-limited |
trustProxy | boolean= false | Trust proxy headers for IP extraction |
standardHeaders | boolean= true | Send RateLimit-* headers |
legacyHeaders | boolean= true | Send X-RateLimit-* headers |
includeRetryAfter | boolean= true | Send Retry-After header on 429 responses |
message | string= 'Too many requests, please try again later.' | Error message for rate-limited responses |
statusCode | number= 429 | HTTP status code for rate-limited responses |
whitelist? | string[] | IPs to skip rate limiting (supports CIDR) |
blacklist? | string[] | IPs to apply stricter limits (supports CIDR) |
blacklistMultiplier | number= 0.5 | Multiplier for blacklisted IP limits (0–1) |
draftIetfHeaders | boolean= false | Send RateLimit-Policy header (IETF draft) |
cleanupInterval | number= 60000 | Interval (ms) for expired entry cleanup |
disableCleanup | boolean= false | Disable automatic expired entry cleanup |
Window Formats
The window option accepts duration strings or raw milliseconds:
'30s'; // 30 seconds
'1m'; // 1 minute
'15m'; // 15 minutes
'1h'; // 1 hour
'1d'; // 1 day
60000; // 1 minute (milliseconds)Supported string units: s, sec, second, seconds, m, min, minute, minutes, h, hr, hour, hours, d, day, days.
Algorithms
Allows controlled bursts while maintaining an average rate:
app.use(
rateLimit({
algorithm: 'token-bucket',
max: 100,
window: '1m',
burstLimit: 20,
})
);Typical use: Public APIs where occasional bursts are acceptable.
Smooth, accurate limiting using a weighted average of current and previous windows:
app.use(
rateLimit({
algorithm: 'sliding-window',
max: 100,
window: '1m',
})
);Typical use: Strict enforcement, billing-sensitive APIs, preventing boundary attacks.
Resets the counter at fixed intervals:
app.use(
rateLimit({
algorithm: 'fixed-window',
max: 100,
window: '1m',
})
);Typical use: Internal APIs, low-overhead use cases.
Clients can hit up to 2x the limit at window boundaries with fixed-window. Use sliding-window for strict enforcement.
Tiered Rate Limits
Apply different limits based on user type:
import { tieredRateLimit } from '@nextrush/rate-limit';
app.use(
tieredRateLimit({
tiers: {
anonymous: { max: 60, window: '1m' },
authenticated: { max: 1000, window: '1m' },
premium: { max: 10000, window: '1m' },
},
tierResolver: (ctx) => ctx.state.user?.tier || 'anonymous',
defaultTier: 'anonymous',
})
);TieredRateLimitOptions (extends RateLimitOptions without max/window)
| Property | Type | Description |
|---|---|---|
tiers | Record<string, TierConfig> | Tier name → { max, window, burstLimit? } |
tierResolver | (ctx: Context) => string | Promise<string> | Resolves the tier for a request |
defaultTier? | string= First key in tiers | Fallback tier if resolver returns an unknown name |
Integration Example
Rate limiting with key generation, whitelist, and event logging:
import { createApp } from '@nextrush/core';
import { rateLimit } from '@nextrush/rate-limit';
const app = createApp();
app.use(
rateLimit({
max: 500,
window: '15m',
algorithm: 'sliding-window',
keyGenerator: (ctx) => ctx.get('X-API-Key') || ctx.ip,
whitelist: ['127.0.0.1', '10.0.0.0/8'],
skip: (ctx) => ctx.path === '/health',
onRateLimited: (ctx, info) => {
console.log(`Rate limited: ${ctx.ip}, resets in ${info.resetIn}s`);
},
})
);Response Headers
Headers are set on every response (not only 429 responses):
Standard headers (IETF draft):
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 30Legacy headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200The Retry-After header is included only on 429 responses.
Proxy Support
When your app runs behind a reverse proxy, the middleware needs to read forwarded IP headers to identify clients correctly.
Only enable trustProxy when your app runs behind a trusted reverse proxy. Untrusted clients can
spoof IP headers to bypass rate limits.
app.use(rateLimit({ trustProxy: true }));When enabled, the middleware checks these headers in priority order:
CF-Connecting-IP(Cloudflare)X-Real-IP(Nginx)X-Forwarded-For(standard proxy)X-Client-IP(Apache)True-Client-IP(Akamai)X-Cluster-Client-IP(Rackspace)Forwarded-ForForwarded(RFC 7239)ctx.ip(socket address)
Custom Store
For distributed deployments, implement the RateLimitStore interface:
import { rateLimit, type RateLimitStore, type StoreEntry } from '@nextrush/rate-limit';
const redisStore: RateLimitStore = {
async get(key: string): Promise<StoreEntry | null> {
/* ... */
},
async set(key: string, entry: StoreEntry, ttlMs: number): Promise<void> {
/* ... */
},
async increment(key: string, ttlMs: number): Promise<number> {
/* ... */
},
async reset(key: string): Promise<void> {
/* ... */
},
// Optional: decrement, cleanup, shutdown
};
app.use(rateLimit({ store: redisStore }));Middleware Methods
The returned middleware exposes methods for programmatic control:
const limiter = rateLimit({ max: 100 });
app.use(limiter);
// Reset a specific key
await limiter.reset('rl:192.168.1.1');
// Get current rate limit info
const info = await limiter.getInfo('rl:192.168.1.1');
// Cleanup on shutdown
await limiter.shutdown();Common Mistakes
Using trustProxy: true without a trusted proxy. Any client can send forged X-Forwarded-For headers. Only enable this behind a reverse proxy you control.
Setting window too short with high max. A window of '1s' with max: 1000 effectively disables rate limiting. Use longer windows with proportional limits.
Forgetting limiter.shutdown(). The in-memory store runs a cleanup timer. Call shutdown() during graceful server shutdown to clear the interval and free resources.
Using in-memory store across multiple instances. Each process has its own store. Requests load-balanced across instances each get full limits. Use a Redis-backed store for multi-instance deployments.
Troubleshooting
Rate limits reset on server restart. The default in-memory store does not persist state. Use a Redis or database-backed store for persistence.
Clients hit limits faster than expected. Check if trustProxy is set correctly. Without it, all proxied requests may share the same socket IP, causing shared limits.
RateLimitValidationError on startup. Options are validated at creation time. Check that max is a positive integer, blacklistMultiplier is between 0 and 1, and window uses a valid format.
Related
- Middleware Overview — All middleware packages
- @nextrush/cors — CORS handling
- @nextrush/helmet — Security headers