@davaux/rate-limit
Limits the number of requests a client can make within a sliding time window. Includes an in-memory store suitable for single-process deployments and a RateLimitStore interface for plugging in Redis or other distributed backends.
Installation
npm install @davaux/rate-limit
Basic usage
// davaux.config.ts
import { defineConfig } from 'davaux/config'
import { rateLimit } from '@davaux/rate-limit'
export default defineConfig({
middleware: [
rateLimit({
windowMs: 60_000, // 1 minute
max: 100, // max 100 requests per window per IP
}),
],
})
When a client exceeds the limit, the middleware responds with 429 Too Many Requests and a Retry-After header indicating when the window resets.
Options
| Option | Type | Default | Description |
|---|---|---|---|
windowMs | number | 60_000 | Time window in milliseconds |
max | number | 100 | Maximum requests per window per key |
keyFn | (ctx) => string | Client IP | Function that returns the rate-limit bucket key |
store | RateLimitStore | MemoryStore | Backend storage for counters |
message | string | 'Too Many Requests' | Response body when limit is exceeded |
statusCode | number | 429 | HTTP status code when limit is exceeded |
skipFn | (ctx) => boolean | — | Return true to skip rate limiting for a request |
Response headers
On every request, the middleware sets:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds until the window resets (only on 429) |
Scoped rate limits
Apply tighter limits to specific routes by using scoped middleware instead of config-level:
// src/routes/api/_middleware.ts — stricter limit for API routes
import { defineMiddleware } from 'davaux'
import { rateLimit } from '@davaux/rate-limit'
const apiLimit = rateLimit({ windowMs: 60_000, max: 30 })
export default defineMiddleware(async (ctx, next) => {
return apiLimit(ctx, next)
})
// src/routes/auth/_middleware.ts — very tight limit for auth endpoints
import { defineMiddleware } from 'davaux'
import { rateLimit } from '@davaux/rate-limit'
const authLimit = rateLimit({
windowMs: 15 * 60_000, // 15 minutes
max: 10, // 10 attempts per 15 minutes per IP
message: 'Too many login attempts. Please try again later.',
})
export default defineMiddleware(async (ctx, next) => {
return authLimit(ctx, next)
})
Custom key function
By default, the client IP is used as the bucket key. Use keyFn to rate-limit by user ID, API key, or any other value:
rateLimit({
windowMs: 60_000,
max: 1000,
keyFn: (ctx) => {
// Rate limit by authenticated user ID, fall back to IP
return ctx.state.user?.id ?? ctx.req.socket.remoteAddress ?? 'unknown'
},
})
Skipping rate limits
Use skipFn to exclude certain requests from the counter:
rateLimit({
windowMs: 60_000,
max: 100,
skipFn: (ctx) => {
// Don't rate limit internal health checks or admin users
return ctx.url.pathname === '/health' || ctx.state.user?.role === 'admin'
},
})
Custom store (Redis example)
Implement the RateLimitStore interface to use a distributed store:
import type { RateLimitStore } from '@davaux/rate-limit'
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
class RedisStore implements RateLimitStore {
async increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }> {
const redisKey = `rl:${key}`
const now = Date.now()
const resetAt = now + windowMs
const count = await redis.incr(redisKey)
if (count === 1) {
await redis.pExpire(redisKey, windowMs)
}
const ttl = await redis.pTtl(redisKey)
return { count, resetAt: now + ttl }
}
async reset(key: string): Promise<void> {
await redis.del(`rl:${key}`)
}
}
export default defineConfig({
middleware: [
rateLimit({
windowMs: 60_000,
max: 100,
store: new RedisStore(),
}),
],
})
MemoryStore
The built-in MemoryStore is suitable for single-process deployments. It automatically cleans up expired entries every minute:
import { MemoryStore } from '@davaux/rate-limit'
rateLimit({
windowMs: 60_000,
max: 100,
store: new MemoryStore(), // same as the default
})
In multi-process or clustered deployments (multiple Node.js workers), use a shared store like Redis so all processes share the same counters.