Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/visible/cruel/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Rate Limiter pattern controls the rate at which operations can be executed using a token bucket algorithm. Each operation consumes a token, and tokens are refilled at a fixed rate. This prevents overwhelming downstream services or exceeding API rate limits.

When to Use

  • Limiting API calls to respect rate limits
  • Controlling resource consumption
  • Preventing abuse or excessive usage
  • Implementing fair usage policies
  • Protecting downstream services from overload

How It Works

1

Initialize Tokens

Start with a bucket of tokens (equal to requests).
2

Consume Token

Each operation consumes one token from the bucket.
3

Refill Tokens

Tokens are refilled at a rate of requests per interval.
4

Reject When Empty

When the bucket is empty, operations are rejected with CruelRateLimitError.

API Reference

Function Signature

function createRateLimiter<T extends AnyFn>(fn: T, options: RateLimiterOptions): T

Options

requests
number
required
Number of requests allowed per interval (bucket size).
interval
number
required
Time interval in milliseconds for token refill.
onLimit
() => void
Callback function executed when a request is rate limited.

Examples

Basic Rate Limiting

import { createRateLimiter } from 'cruel'

const fetchUser = async (id: string) => {
  const response = await fetch(`https://api.example.com/users/${id}`)
  return response.json()
}

// Allow 10 requests per second
const limitedFetch = createRateLimiter(fetchUser, {
  requests: 10,
  interval: 1000,
})

try {
  const user = await limitedFetch('123')
} catch (error) {
  if (error instanceof CruelRateLimitError) {
    console.log(`Rate limited, retry after ${error.retryAfter}s`)
  }
}

API Client with Rate Limiting

import { createRateLimiter, CruelRateLimitError } from 'cruel'

class APIClient {
  private apiCall: ReturnType<typeof createRateLimiter>

  constructor(
    private apiKey: string,
    requestsPerMinute: number = 60
  ) {
    this.apiCall = createRateLimiter(
      this.makeRequest.bind(this),
      {
        requests: requestsPerMinute,
        interval: 60000,  // 1 minute
        onLimit: () => {
          console.warn('API rate limit reached')
          metrics.increment('api.rate_limited')
        },
      }
    )
  }

  private async makeRequest(endpoint: string) {
    const response = await fetch(endpoint, {
      headers: { 'Authorization': `Bearer ${this.apiKey}` },
    })
    return response.json()
  }

  async get(endpoint: string) {
    return this.apiCall(endpoint)
  }
}

const client = new APIClient('api-key', 100)

Different Rate Limits per Tier

const rateLimitsByTier = {
  free: { requests: 10, interval: 60000 },    // 10/min
  basic: { requests: 100, interval: 60000 },  // 100/min
  premium: { requests: 1000, interval: 60000 }, // 1000/min
}

function createAPIClient(tier: keyof typeof rateLimitsByTier) {
  const limits = rateLimitsByTier[tier]
  
  return createRateLimiter(
    makeAPIRequest,
    {
      ...limits,
      onLimit: () => {
        console.log(`Rate limit reached for ${tier} tier`)
      },
    }
  )
}

const freeAPI = createAPIClient('free')
const premiumAPI = createAPIClient('premium')

Per-User Rate Limiting

const userRateLimiters = new Map<string, ReturnType<typeof createRateLimiter>>()

function getUserRateLimiter(userId: string) {
  if (!userRateLimiters.has(userId)) {
    userRateLimiters.set(userId, createRateLimiter(
      processUserRequest,
      {
        requests: 100,   // 100 requests
        interval: 60000, // per minute
        onLimit: () => {
          console.log(`User ${userId} rate limited`)
        },
      }
    ))
  }
  return userRateLimiters.get(userId)!
}

async function handleRequest(userId: string, data: any) {
  const limiter = getUserRateLimiter(userId)
  return limiter(data)
}

Burst Handling

// Allow bursts of up to 100 requests, but sustain only 10/second
const burstAPI = createRateLimiter(apiCall, {
  requests: 100,   // Bucket size (burst capacity)
  interval: 10000, // Refill rate: 100 tokens per 10s = 10/s sustained
})

// Can immediately make 100 requests (burst)
// Then limited to ~10 per second

Multiple Rate Limiters

// Limit by both requests per second AND requests per minute
const perSecondLimiter = createRateLimiter(apiCall, {
  requests: 10,
  interval: 1000,
})

const perMinuteLimiter = createRateLimiter(perSecondLimiter, {
  requests: 500,
  interval: 60000,
})

// Must pass both rate limiters
await perMinuteLimiter(data)

Combining with Other Patterns

Rate Limiter + Retry

import { createRateLimiter, withRetry, CruelRateLimitError } from 'cruel'

// Retry when rate limited
const resilientAPI = withRetry(
  createRateLimiter(apiCall, {
    requests: 10,
    interval: 1000,
  }),
  {
    attempts: 3,
    delay: 1000,
    backoff: 'exponential',
    retryIf: (error) => error instanceof CruelRateLimitError,
  }
)

Rate Limiter + Queue

import { createRateLimiter, createBulkhead } from 'cruel'

// Queue requests when rate limited
const queuedAPI = createBulkhead(
  createRateLimiter(apiCall, {
    requests: 10,
    interval: 1000,
  }),
  {
    maxConcurrent: 1,
    maxQueue: 100,
  }
)

Rate Limiter + Circuit Breaker

import { createRateLimiter, createCircuitBreaker } from 'cruel'

const resilientAPI = createCircuitBreaker(
  createRateLimiter(apiCall, {
    requests: 100,
    interval: 60000,
  }),
  {
    threshold: 5,
    timeout: 30000,
  }
)

With Compose

import { cruel } from 'cruel'

const resilientAPI = cruel.compose(apiCall, {
  rateLimiter: {
    requests: 100,
    interval: 60000,
    onLimit: () => console.warn('Rate limited'),
  },
  retry: {
    attempts: 3,
    backoff: 'exponential',
  },
  bulkhead: {
    maxConcurrent: 10,
  },
})

Advanced Examples

Adaptive Rate Limiter

class AdaptiveRateLimiter {
  private currentLimit: number
  private errorRate: number = 0

  constructor(
    private baseLimit: number,
    private interval: number
  ) {
    this.currentLimit = baseLimit
  }

  createLimiter<T extends AnyFn>(fn: T) {
    return createRateLimiter(fn, {
      requests: this.currentLimit,
      interval: this.interval,
    })
  }

  recordResponse(statusCode: number) {
    if (statusCode === 429) {
      // Rate limited by server, reduce limit
      this.errorRate = Math.min(1, this.errorRate + 0.1)
      this.currentLimit = Math.max(1, Math.floor(this.currentLimit * 0.8))
    } else if (statusCode < 400) {
      // Success, slowly increase limit
      this.errorRate = Math.max(0, this.errorRate - 0.01)
      if (this.errorRate < 0.05 && this.currentLimit < this.baseLimit) {
        this.currentLimit = Math.min(this.baseLimit, this.currentLimit + 1)
      }
    }
  }
}

const adaptive = new AdaptiveRateLimiter(100, 60000)
const apiCall = adaptive.createLimiter(fetchData)

// After each call
const response = await apiCall()
adaptive.recordResponse(response.status)

Distributed Rate Limiter (Redis)

import Redis from 'ioredis'

class RedisRateLimiter {
  constructor(private redis: Redis, private key: string) {}

  async checkLimit(requests: number, interval: number): Promise<boolean> {
    const now = Date.now()
    const windowStart = now - interval

    // Remove old entries
    await this.redis.zremrangebyscore(this.key, 0, windowStart)

    // Count requests in current window
    const count = await this.redis.zcard(this.key)

    if (count >= requests) {
      return false
    }

    // Add current request
    await this.redis.zadd(this.key, now, `${now}-${Math.random()}`)
    await this.redis.expire(this.key, Math.ceil(interval / 1000))

    return true
  }
}

function createDistributedRateLimiter<T extends AnyFn>(
  fn: T,
  redis: Redis,
  key: string,
  options: RateLimiterOptions
): T {
  const limiter = new RedisRateLimiter(redis, key)

  return async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    const allowed = await limiter.checkLimit(options.requests, options.interval)

    if (!allowed) {
      options.onLimit?.()
      throw new CruelRateLimitError(Math.ceil(options.interval / 1000))
    }

    return fn(...args) as ReturnType<T>
  }
}

Weighted Rate Limiter

interface WeightedRateLimiterOptions extends RateLimiterOptions {
  getWeight?: (...args: any[]) => number
}

function createWeightedRateLimiter<T extends AnyFn>(
  fn: T,
  options: WeightedRateLimiterOptions
): T {
  let tokens = options.requests
  let lastRefill = Date.now()

  const refill = () => {
    const now = Date.now()
    const elapsed = now - lastRefill
    const tokensToAdd = Math.floor(elapsed / options.interval) * options.requests
    if (tokensToAdd > 0) {
      tokens = Math.min(options.requests, tokens + tokensToAdd)
      lastRefill = now
    }
  }

  return async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    refill()

    const weight = options.getWeight ? options.getWeight(...args) : 1

    if (tokens < weight) {
      options.onLimit?.()
      throw new CruelRateLimitError(Math.ceil(options.interval / 1000))
    }

    tokens -= weight
    return fn(...args) as ReturnType<T>
  }
}

const weightedAPI = createWeightedRateLimiter(
  apiCall,
  {
    requests: 100,
    interval: 60000,
    getWeight: (request) => {
      // Batch requests cost more tokens
      return request.batch ? 10 : 1
    },
  }
)

Error Handling

Rate limiter throws CruelRateLimitError when limit is exceeded:
import { CruelRateLimitError } from 'cruel'

try {
  await limitedAPI(data)
} catch (error) {
  if (error instanceof CruelRateLimitError) {
    console.log(`Rate limited for ${error.retryAfter} seconds`)
    // error.status === 429
    // error.retryAfter - seconds to wait
  }
}

Best Practices

  • Match external rate limits: Set limits to match API provider’s limits
  • Add buffer: Set slightly lower than actual limit to account for timing variance
  • Use per-user limiters: Prevent one user from consuming all quota
  • Implement retry logic: Handle rate limit errors gracefully
  • Monitor token usage: Track how close to limits you’re operating
  • Log rate limit hits: Help identify usage patterns
  • Consider burst capacity: Allow short bursts while maintaining sustained rate
  • Use distributed limiters: For multi-instance deployments

Rate Limit Strategies

Fixed Window

// Simple but allows burst at window boundaries
createRateLimiter(fn, { requests: 100, interval: 60000 })

Token Bucket (Default)

// Smooths traffic, allows controlled bursts
createRateLimiter(fn, { requests: 100, interval: 60000 })

Sliding Window (Custom)

// Most accurate but more complex
// Implement using custom limiter with timestamp tracking

Configuration Examples

API ProviderRequestsIntervalNotes
Twitter300900000300 requests per 15 minutes
GitHub500036000005000 requests per hour
Stripe1001000100 requests per second
OpenAI606000060 requests per minute
Custom API100060000Typical REST API limit

Common Rate Limits

ScenarioRequestsIntervalRate
Free tier106000010/min
Basic tier10060000100/min
Premium tier1000600001000/min
Internal API1001000100/sec
Public API10100010/sec