OSS Ratelimit

Manage Multiple Limiters (Recommended)

Learn the recommended pattern for defining and applying different rate limits to various API endpoints, user tiers, or specific actions within your application using oss-ratelimit.

Client & Registry Management

Managing multiple rate limiters (e.g., for different API endpoints or user tiers) and efficiently handling Redis connections is crucial. oss-ratelimit provides a powerful registry system for this.

ratelimit.ts
middleware.ts
package.json
.env

1. Initialize the Registry (initRateLimit)

Create a registry instance, optionally providing default Redis options. Define the names your limiters will use with a type union for type safety.

import {
  initRateLimit,
  RateLimitBuilder,
  RegisterConfigParam, // Type for registration config
  slidingWindow,      // Example algorithm
  fixedWindow,
} from 'oss-ratelimit';
 
// Define names for your different limiters (enhances type safety)
export type LimiterName = 'apiGeneral' | 'apiExpensive' | 'loginAttempts';
 
// Create the registry instance
// It's recommended to use environment variables for Redis connection details
export const rateLimiterRegistry: RateLimitBuilder<LimiterName> = initRateLimit<LimiterName>({
  // Default Redis options applied to all limiters unless overridden
  defaultRedisOptions: {
    url: process.env.RATELIMIT_REDIS_URL || 'redis://localhost:6379',
    // password: process.env.RATELIMIT_REDIS_PASSWORD,
    // database: 0, // Default database
  },
});
 
// Define configurations for each named limiter
export const limiterConfigs: Record<LimiterName, RegisterConfigParam> = {
  apiGeneral: {
    limiter: slidingWindow(50, '30 s'), // 50 requests per 30 seconds
    prefix: 'rl_api_gen', // Unique prefix helps organize Redis keys
    analytics: true,      // Enable analytics for this limiter
  },
  apiExpensive: {
    limiter: fixedWindow(5, '1 m'),     // 5 requests per minute
    prefix: 'rl_api_exp',
    timeout: 500,                      // Shorter Redis timeout for this one
  },
  loginAttempts: {
    limiter: fixedWindow(5, '15 m'),    // 5 login attempts per 15 minutes
    prefix: 'rl_login',
    // Example: Using a different Redis DB or instance for sensitive limits
    // envRedisKey: 'LOGIN_REDIS_URL', // Or use 'redis' options:
    // redis: { database: 1 }
  },
};

You can eagerly initialize all defined limiters on application startup using initializeLimiters. This ensures Redis connections are established and configurations are validated early.

Initialize Limiters Eagerly
import { initializeLimiters } from 'oss-ratelimit';
// ... (previous registry setup code) ...
 
// Eagerly initialize all limiters defined in limiterConfigs
// Handle the promise appropriately (e.g., in your server startup logic)
export const initializedLimitersPromise = initializeLimiters({
  registry: rateLimiterRegistry,
  configs: limiterConfigs,
  verbose: process.env.NODE_ENV !== 'production', // Log progress in dev
  throwOnError: true, // Recommended: Stop startup if a limiter fails
});
 
// You can add listeners to the registry *before* initialization
rateLimiterRegistry.on('limiterRegister', ({ name, clientKey }) => {
  console.log(`✅ Limiter "${name}" registered using Redis client: ${clientKey}`);
});
rateLimiterRegistry.on('redisError', ({ clientKey, error }) => {
   console.error(`❌ Redis Error for client ${clientKey}:`, error);
});
rateLimiterRegistry.on('limiterError', ({ name, error }) => {
   console.error(`❌ Failed to initialize limiter "${name}":`, error);
});
 
 
// Handle potential initialization errors globally if needed
initializedLimitersPromise.catch(error => {
   console.error("💥 Critical error during rate limiter initialization:", error);
   // Potentially exit the application or disable features relying on rate limiting
   process.exit(1);
});

Alternatively, limiters will be initialized automatically (and lazily) the first time registry.register(name) or registry.get(name) is called for a specific name if not already initialized.

3. Accessing Limiters

Use registry.get(name) to retrieve an initialized limiter instance.

Reliably getting the real client IP behind proxies requires checking specific headers. Use a dedicated utility function for this.

import type { NextApiRequest, NextApiResponse } from 'next';
import { rateLimiterRegistry, LimiterName } from '@/lib/ratelimit'; // Adjust path
import { getIpFromRequest } from '@/utils/getIpFromRequest';
 
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const ip = getIpFromRequest(req);
  if (!ip) return res.status(400).json({ error: 'Cannot determine IP.' });
 
  try {
    // Get the specific, pre-initialized limiter instance by name
    const limiter = rateLimiterRegistry.get('apiExpensive'); // Throws if not initialized
 
    const { success, remaining } = await limiter.limit(ip);
 
    // Set headers (implementation omitted for brevity)
    // ... set X-RateLimit-* headers ...
 
    if (!success) {
      // ... return 429 ...
      return res.status(429).json({ error: 'Too many requests for expensive route.' });
    }
 
    // --- Proceed with expensive logic ---
    res.status(200).json({ result: 'Expensive operation completed!' });
 
  } catch (error: any) {
    // Handle errors (e.g., if get() throws, or limit() fails)
    console.error("Rate limit error on expensive route:", error);
    return res.status(500).json({ error: 'Internal Server Error' });
  }
}

You can also use the helper functions getInitializedLimiter or createLimiterAccessor for convenience:

Create Limiter Accessor
import { createLimiterAccessor } from 'oss-ratelimit';
// ... (previous registry setup code) ...
 
// Create a type-safe getter function bound to your registry
// Ensure this is called *after* limiters are likely initialized, or handle errors
export const getLimiter = createLimiterAccessor(rateLimiterRegistry);
 
// Usage elsewhere:
// import { getLimiter } from '@/lib/ratelimit';
// const generalApiLimiter = getLimiter('apiGeneral');

4. Redis Client Reuse

The registry automatically manages and reuses Redis client connections based on the configuration (RedisOptions or envRedisKey). This prevents creating excessive connections to your Redis server. You can inspect which client a limiter is using via the limiterRegister event or programmatically if needed (though usually not required).

Advanced Usage

Blocking (block)

Wait until a request is allowed, up to a configurable maximum time.

Using limiter.block()
import { RateLimitExceededError } from 'oss-ratelimit';
 
const identifier = 'user_abc';
const limiter = rateLimiterRegistry.get('apiGeneral');
 
try {
  // Wait up to 3 seconds for the rate limit to allow the request
  const response = await limiter.block(identifier, {
    maxWaitMs: 3000, // Default 5000ms
    // maxAttempts: 10, // Default 50
    // retryDelayMs: 50 // Default 100ms (base delay, backs off automatically)
  });
  console.log(`Request allowed after waiting. Remaining: ${response.remaining}`);
  // Proceed with action...
 
} catch (error) {
  if (error instanceof RateLimitExceededError) {
    console.warn(`Rate limit exceeded for ${error.identifier} even after waiting. Retry after ${error.retryAfter}s.`);
    // Handle timeout (e.g., return an error to the user)
  } else {
    console.error("Error during block:", error);
    // Handle other errors
  }
}

Resetting Limits (reset)

Manually clear the rate limit count for a specific identifier. Useful for testing or specific application logic.

Using limiter.reset()
const identifierToReset = 'user_xyz';
const limiter = rateLimiterRegistry.get('loginAttempts');
 
try {
  const success = await limiter.reset(identifierToReset);
  if (success) {
    console.log(`Rate limit reset successfully for ${identifierToReset}`);
  } else {
     console.warn(`Rate limit reset failed for ${identifierToReset} (maybe due to Redis issue and failOpen=true?)`);
  }
} catch (error) {
   console.error(`Failed to reset rate limit for ${identifierToReset}:`, error);
}

Checking Status (getStats, check)

Query the current state of the rate limit for an identifier without consuming a request token/count.

Using limiter.getStats() and limiter.check()
const identifier = 'ip_1.2.3.4';
const limiter = rateLimiterRegistry.get('apiGeneral');
 
// Get detailed stats
const stats = await limiter.getStats(identifier);
console.log(`Stats for ${identifier}: Used=${stats.used}, Remaining=${stats.remaining}, Limit=${stats.limit}, ResetsAt=${new Date(stats.reset).toISOString()}`);
 
// Simple check if allowed
const isAllowed = await limiter.check(identifier);
if (isAllowed) {
  console.log(`${identifier} has requests remaining.`);
} else {
  console.log(`${identifier} is currently rate limited.`);
}

Configuration Options

Configure the Ratelimit instance or registry registration:

PropTypeDefault
silent?
boolean
false
failOpen?
boolean
false
ephemeralCacheTTL?
number
60000
ephemeralCache?
boolean
true
timeout?
number
1000
analytics?
boolean
false
prefix?
string
oss-ratelimit
limiter?
FixedWindowOptions | SlidingWindowOptions | TokenBucketOptions
-
redis?
RedisClientType | RedisOptions | { envRedisKey: string }
-

Redis Options

Passed directly to redis.createClient or used by getRedisClient:

PropTypeDefault
reconnectStrategy?
-
connectTimeout?
number
5000
tls?
boolean
-
database?
number
-
password?
string
-
username?
string
-
port?
number
-
host?
string
-
url?
string
-

On this page