Skip to content

Rate Limiting ​

Production-ready rate limiting middleware for YasuiJS applications. Protects your API from abuse by limiting the number of requests per time window, with support for custom storage backends and flexible key generation.

Installation ​

bash
npm install @yasui/rate-limit
bash
pnpm add @yasui/rate-limit
bash
bun add @yasui/rate-limit
bash
deno add jsr:@yasui/rate-limit

Overview ​

The @yasui/rate-limit package provides a flexible rate limiting middleware with advanced features including:

  • Configurable limits - Set max requests per time window
  • In-memory store - Built-in storage with automatic cleanup
  • Extensible storage - Redis, database, or custom store support
  • Custom key generation - Rate limit by IP, API key, user ID, or custom logic
  • Standard headers - RFC 6585 compliant rate limit headers
  • Skip logic - Whitelist specific requests
  • Custom handlers - Override default 429 responses

Important: This is a functional middleware (not class-based). It works alongside YasuiJS class middlewares and should be registered in the global middlewares array.

Quick Start ​

Basic Usage ​

typescript
import yasui from 'yasui';
import { rateLimit } from '@yasui/rate-limit';

yasui.createServer({
  middlewares: [
    rateLimit({
      max: 100,       // 100 requests
      windowMs: 60000 // per minute
    })
  ],
  controllers: [UserController]
});

Configuration ​

The rateLimit() function accepts a configuration object with the following options:

max ​

Maximum number of requests allowed per time window.

  • Type: number
  • Default: 100
  • Example:
typescript
rateLimit({
  max: 1000  // Allow 1000 requests per window
})

windowMs ​

Time window duration in milliseconds.

  • Type: number
  • Default: 60000 (1 minute)
  • Examples:
typescript
// 1 minute
rateLimit({ max: 100, windowMs: 60000 })

// 15 minutes
rateLimit({ max: 500, windowMs: 15 * 60 * 1000 })

// 1 hour
rateLimit({ max: 5000, windowMs: 60 * 60 * 1000 })

keyGenerator ​

Custom function to generate rate limit keys. By default, uses client IP address.

  • Type: (req: YasuiRequest) => string
  • Default: Uses X-Forwarded-For, X-Real-IP, or 'unknown'
  • Examples:
typescript
// Rate limit by API key
rateLimit({
  max: 1000,
  windowMs: 3600000,
  keyGenerator: (req) => {
    return req.headers.get('x-api-key') ?? 'anonymous';
  }
})

// Rate limit by user ID (from custom auth middleware)
rateLimit({
  max: 500,
  windowMs: 60000,
  keyGenerator: (req) => {
    return req.userId ?? 'anonymous';  // Assuming auth middleware sets req.userId
  }
})

// Combine multiple factors
rateLimit({
  max: 100,
  windowMs: 60000,
  keyGenerator: (req) => {
    const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ?? 'unknown';
    const endpoint = new URL(req.url).pathname;
    return `${ip}:${endpoint}`;
  }
})

store ​

Custom storage backend for rate limit data. By default, uses in-memory store.

  • Type: RateLimitStore
  • Default: MemoryStore
  • Interface:
typescript
interface RateLimitStore {
  increment: (key: string) => Promise<number> | number;
  reset: (key: string) => Promise<void> | void;
}

Example (Redis):

typescript
import { createClient } from 'redis';
import type { RateLimitStore } from '@yasui/rate-limit';

class RedisStore implements RateLimitStore {
  constructor(
    private client: ReturnType<typeof createClient>,
    private windowMs: number
  ) {}

  async increment(key: string): Promise<number> {
    const redisKey = `ratelimit:${key}`;
    const count = await this.client.incr(redisKey);

    if (count === 1) {
      await this.client.expire(redisKey, Math.ceil(this.windowMs / 1000));
    }

    return count;
  }

  async reset(key: string): Promise<void> {
    await this.client.del(`ratelimit:${key}`);
  }
}

const redisClient = createClient();
await redisClient.connect();

yasui.createServer({
  middlewares: [
    rateLimit({
      max: 100,
      windowMs: 60000,
      store: new RedisStore(redisClient, 60000)
    })
  ],
  controllers: [UserController]
});

handler ​

Custom handler for rate limit exceeded responses. Follows YasuiJS patterns: throw HttpError, return data (auto-converts to JSON), or return Response for custom formats.

  • Type: (req: YasuiRequest, limit: number, remaining: number, resetTime: number) => Response | unknown | Promise<Response | unknown>
  • Default: Returns JSON 429 response with standard headers
  • Examples:
typescript
import { HttpError } from 'yasui';

// Throw HttpError (recommended for JSON errors)
rateLimit({
  max: 100,
  windowMs: 60000,
  handler: (req, limit) => {
    throw new HttpError(429, 'Too many requests. Please slow down.');
  }
})

// Return object (auto-converts to JSON with 429 status)
rateLimit({
  max: 100,
  windowMs: 60000,
  handler: (req, limit, remaining, resetTime) => {
    return {
      error: 'Rate limit exceeded',
      limit,
      remaining,
      resetTime: Math.ceil(resetTime / 1000)
    };
  }
})

// Return Response for custom format (HTML, XML, etc.)
rateLimit({
  max: 100,
  windowMs: 60000,
  handler: (req) => {
    const acceptsHtml = req.headers.get('accept')?.includes('text/html');

    if (acceptsHtml) {
      return new Response(
        '<h1>Too Many Requests</h1><p>Please try again later.</p>',
        {
          status: 429,
          headers: { 'Content-Type': 'text/html' }
        }
      );
    }

    throw new HttpError(429, 'Rate limit exceeded');
  }
})

skip ​

Function to skip rate limiting for specific requests.

  • Type: (req: YasuiRequest) => boolean | Promise<boolean>
  • Default: undefined
  • Examples:
typescript
// Skip internal requests
rateLimit({
  max: 100,
  windowMs: 60000,
  skip: (req) => {
    return req.headers.get('x-internal-request') === 'true';
  }
})

// Skip authenticated admin users
rateLimit({
  max: 100,
  windowMs: 60000,
  skip: async (req) => {
    const token = req.headers.get('authorization');
    const user = await validateToken(token);
    return user?.role === 'admin';
  }
})

// Skip health check endpoints
rateLimit({
  max: 100,
  windowMs: 60000,
  skip: (req) => {
    const path = new URL(req.url).pathname;
    return path === '/health' || path === '/ping';
  }
})

standardHeaders ​

Include standard rate limit headers in responses.

  • Type: boolean
  • Default: true
  • Headers added:
    • RateLimit-Limit: Maximum requests per window
    • RateLimit-Remaining: Requests remaining
    • RateLimit-Reset: Unix timestamp when limit resets
    • Retry-After: Seconds until reset (only on 429 responses)
typescript
// Disable standard headers
rateLimit({
  max: 100,
  windowMs: 60000,
  standardHeaders: false
})

How It Works ​

Request Tracking ​

The middleware tracks requests using a sliding window algorithm:

  1. Extract key: Uses keyGenerator to identify the requester (IP, API key, etc.)
  2. Increment counter: Stores request timestamp in the configured store
  3. Check limit: Compares request count against max
  4. Allow or deny: Returns 429 if exceeded, otherwise continues

Response Headers ​

When standardHeaders: true, responses include:

http
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 1699564800
Content-Type: application/json

When rate limit exceeded:

http
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 1699564800
Retry-After: 45
Content-Type: application/json

{"error":"Too Many Requests","message":"Rate limit exceeded. Try again in 45 seconds."}

Memory Store Cleanup ​

The built-in MemoryStore automatically cleans up expired entries:

  • Removes timestamps older than windowMs on each request
  • Performs full cleanup when store size exceeds 10,000 entries
  • Uses efficient filtering to minimize memory usage

Key Generation ​

Default key generation follows this priority:

  1. X-Forwarded-For header (first IP in chain)
  2. X-Real-IP header
  3. 'unknown' fallback

Production Tip: Behind a reverse proxy (nginx, Cloudflare), ensure X-Forwarded-For is set correctly. Consider validating the header to prevent spoofing.

Security Best Practices ​

1. Use Conservative Limits ​

typescript
// ❌ TOO GENEROUS
rateLimit({ max: 100000, windowMs: 60000 })

// ✅ REASONABLE
rateLimit({ max: 100, windowMs: 60000 })

2. Protect Sensitive Endpoints ​

typescript
// ✅ GOOD - Strict limits for auth endpoints
const authLimit = rateLimit({ max: 5, windowMs: 15 * 60 * 1000 });

@Controller('/auth')
export class AuthController {
  @Post('/login', authLimit)
  login() {}

  @Post('/reset-password', authLimit)
  resetPassword() {}
}

3. Validate Key Generator Input ​

Default behavior: Uses X-Forwarded-For → X-Real-IP → request signature hash. Behind a reverse proxy, ensure headers are set correctly.

typescript
// ✅ PRODUCTION - Validate trusted proxy
rateLimit({
  keyGenerator: (req) => {
    const forwarded = req.headers.get('x-forwarded-for');

    // Only trust X-Forwarded-For from known proxies
    if (forwarded && isTrustedProxy(req)) {
      return forwarded.split(',')[0].trim();
    }

    // Fallback: don't share rate limit across unknowns
    const userAgent = req.headers.get('user-agent') || '';
    return `fallback:${hashUserAgent(userAgent)}`;
  }
})

Note: Default key generator uses request signature (User-Agent + Accept-Language) as fallback to prevent all unknown requests sharing the same rate limit.

4. Use Persistent Storage in Production ​

typescript
// ❌ RISKY - In-memory store (lost on restart)
rateLimit({ max: 100, windowMs: 60000 })

// ✅ PRODUCTION - Redis store (persistent)
rateLimit({
  max: 100,
  windowMs: 60000,
  store: new RedisStore(redisClient, 60000)
})

5. Monitor Rate Limit Violations ​

typescript
import { HttpError } from 'yasui';

rateLimit({
  max: 100,
  windowMs: 60000,
  handler: async (req, limit) => {
    // Log violation for monitoring
    console.warn(`Rate limit exceeded: ${req.url}`);
    await logSecurityEvent({
      type: 'rate_limit_exceeded',
      ip: req.headers.get('x-forwarded-for'),
      endpoint: req.url
    });

    throw new HttpError(429, 'Too Many Requests');
  }
})

Technical Details ​

The rate limit middleware can be applied at all levels (application, controller, endpoint). See Middlewares Reference for details on middleware usage levels and execution order.

Store Interface ​

Custom stores must implement:

typescript
interface RateLimitStore {
  increment: (key: string) => Promise<number> | number;
  reset: (key: string) => Promise<void> | void;
}
  • increment: Increment counter and return current count
  • reset: Clear counter for specific key

Performance Optimizations ​

  • Timestamps filtered efficiently (only valid entries kept)
  • Dual cleanup strategy: time-based (every 60s) + size-based (>10k keys)
  • LRU eviction when max size exceeded (removes 20% oldest entries)
  • Synchronous increment for in-memory store (no await overhead)
  • Headers injected without cloning response body

Memory Safety: In-memory store limited to 10,000 keys max with automatic LRU eviction. For high-traffic production (>10k unique IPs/hour), use Redis store.

Compliance ​

  • RFC 6585: 429 Too Many Requests status code
  • Draft RFC: RateLimit-* headers (IETF draft standard)
  • Industry Standards: Retry-After header for client retry logic

See Also ​

Released under the AGPL v3 License.