Skip to content

stacksjs/ts-rate-limiter

Repository files navigation

Social Card of this repo

npm version GitHub Actions Commitizen friendly

ts-rate-limiter

A high-performance, flexible rate limiting library for TypeScript and Bun.

Features

  • Multiple Rate Limiting Algorithms:

    • Fixed Window
    • Sliding Window
    • Token Bucket
  • Storage Providers:

    • In-memory storage (default)
    • Redis storage
  • Performance Optimizations:

    • Batch operations
    • LUA scripting for Redis
    • Automatic cleanup of expired records
  • Flexible Configuration:

    • Custom key generators
    • Skip and handler functions
    • Draft mode (record but don't block)
    • Standard and legacy headers

Installation

bun add ts-rate-limiter

Basic Usage

import { RateLimiter } from 'ts-rate-limiter'

// Create a rate limiter with 100 requests per minute
const limiter = new RateLimiter({
  windowMs: 60 * 1000, // 1 minute
  maxRequests: 100 // limit each IP to 100 requests per windowMs
})

// In your Bun server
const server = Bun.serve({
  port: 3000,
  async fetch(req) {
    // Use as middleware
    const limiterResponse = await limiter.middleware()(req)
    if (limiterResponse) {
      return limiterResponse
    }

    // Continue with your normal request handling
    return new Response('Hello World')
  }
})

Configuration Options

// Create a rate limiter with more options
const limiter = new RateLimiter({
  windowMs: 15 * 60 * 1000, // 15 minutes
  maxRequests: 100, // limit each IP to 100 requests per windowMs
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
  algorithm: 'sliding-window', // 'fixed-window', 'sliding-window', or 'token-bucket'

  // Skip certain requests
  skip: (request) => {
    return request.url.includes('/api/health')
  },

  // Custom handler for rate-limited requests
  handler: (request, result) => {
    return new Response('Too many requests, please try again later.', {
      status: 429,
      headers: {
        'Retry-After': Math.ceil(result.remaining / 1000).toString()
      }
    })
  },

  // Custom key generator
  keyGenerator: (request) => {
    // Use user ID if available, otherwise fall back to IP
    const userId = getUserIdFromRequest(request)
    return userId || request.headers.get('x-forwarded-for') || '127.0.0.1'
  },

  // Draft mode - don't actually block requests, just track them
  draftMode: process.env.NODE_ENV !== 'production'
})

Using Redis Storage

import { RateLimiter, RedisStorage } from 'ts-rate-limiter'

// Create Redis client
const redis = createRedisClient() // Use your Redis client library

// Create Redis storage
const storage = new RedisStorage({
  client: redis,
  keyPrefix: 'ratelimit:',
  enableSlidingWindow: true
})

// Create rate limiter with Redis storage
const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  maxRequests: 100,
  storage
})

Distributed Rate Limiting

For applications running on multiple servers, use Redis storage to ensure consistent rate limiting:

import { createClient } from 'redis'
import { RateLimiter, RedisStorage } from 'ts-rate-limiter'

// Create and connect Redis client
const redisClient = createClient({
  url: 'redis://redis-server:6379'
})
await redisClient.connect()

// Create Redis storage with sliding window for more accuracy
const storage = new RedisStorage({
  client: redisClient,
  keyPrefix: 'app:ratelimit:',
  enableSlidingWindow: true
})

// Create rate limiter with Redis storage
const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  maxRequests: 100,
  storage,
  algorithm: 'sliding-window'
})

// Make sure to handle Redis errors
redisClient.on('error', (err) => {
  console.error('Redis error:', err)
  // You might want to fall back to memory storage in case of Redis failure
})

Best Practices

Choosing the Right Algorithm

  • Fixed Window: Simplest approach, best for non-critical rate limiting with good performance
  • Sliding Window: More accurate, prevents traffic spikes at window boundaries
  • Token Bucket: Best for APIs that need to allow occasional bursts of traffic

Performance Considerations

  • Use memory storage for single-instance applications

  • Use Redis storage for distributed applications

  • Enable automatic cleanup for long-running applications:

    const memoryStorage = new MemoryStorage({
      enableAutoCleanup: true,
      cleanupIntervalMs: 60000 // cleanup every minute
    })
  • Use batch operations for bulk processing:

    // Process multiple keys at once
    const results = await storage.batchIncrement(['key1', 'key2', 'key3'], windowMs)

Rate Limiting by User or IP

By default, the rate limiter uses IP addresses. For user-based rate limiting:

const userRateLimiter = new RateLimiter({
  windowMs: 60 * 1000,
  maxRequests: 100,
  keyGenerator: (request) => {
    // Extract user ID from auth token, session, etc.
    const userId = getUserIdFromRequest(request)
    if (!userId) {
      throw new Error('User not authenticated')
    }
    return `user:${userId}`
  },
  skipFailedRequests: true // Allow unauthenticated requests to bypass rate limiting
})

Benchmarks

Performance comparison of different algorithms and storage providers:

Algorithm Storage Requests/sec Latency (avg)
Fixed Window Memory 2,742,597 0.000365ms
Sliding Window Memory 10,287 0.097203ms
Token Bucket Memory 5,079,977 0.000197ms
Fixed Window Redis 10,495 0.095277ms
Sliding Window Redis 1,843 0.542406ms
Token Bucket Redis 4,194,263 0.000238ms

Benchmarked on Bun v1.2.9, MacBook Pro M3, 100,000 requests per test for Memory, 10,000 requests per test for Redis. All tests performed with Redis running locally.

Algorithms

Fixed Window

The traditional rate limiting approach that counts requests in a fixed time window.

Sliding Window

More accurate limiting by considering the distribution of requests within the window.

Token Bucket

Offers a smoother rate limiting experience by focusing on request rates rather than fixed counts.

Testing

bun test

Changelog

Please see our releases page for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discussions on GitHub

For casual chit-chat with others using this package:

Join the Stacks Discord Server

Postcardware

"Software that is free, but hopes for a postcard." We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.

Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎

Sponsors

We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.

License

The MIT License (MIT). Please see LICENSE for more information.

Made with πŸ’™