@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

OptionTypeDefaultDescription
windowMsnumber60_000Time window in milliseconds
maxnumber100Maximum requests per window per key
keyFn(ctx) => stringClient IPFunction that returns the rate-limit bucket key
storeRateLimitStoreMemoryStoreBackend storage for counters
messagestring'Too Many Requests'Response body when limit is exceeded
statusCodenumber429HTTP status code when limit is exceeded
skipFn(ctx) => booleanReturn true to skip rate limiting for a request

Response headers

On every request, the middleware sets:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window
X-RateLimit-RemainingRemaining requests in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds 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.