or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdindex.mdmiddleware.mdstores.md
tile.json

stores.mddocs/

Storage Backends

This document covers storage options for express-rate-limit, including the built-in MemoryStore and the interface for creating custom storage backends.

Built-in MemoryStore

The default storage backend that keeps rate limit data in memory.

import { MemoryStore } from "express-rate-limit";

class MemoryStore implements Store {
  constructor(validations?: Validations);
  
  // Store interface methods
  init(options: Options): void;
  get(key: string): Promise<ClientRateLimitInfo | undefined>;
  increment(key: string): Promise<ClientRateLimitInfo>;
  decrement(key: string): Promise<void>;
  resetKey(key: string): Promise<void>;
  resetAll(): Promise<void>;
  shutdown(): void;
  
  // Properties
  localKeys: boolean; // Always true for MemoryStore
  windowMs: number;
  previous: Map<string, Client>;
  current: Map<string, Client>;
  interval?: NodeJS.Timeout;
}

interface ClientRateLimitInfo {
  totalHits: number;
  resetTime: Date | undefined;
}

Basic Usage

import rateLimit, { MemoryStore } from "express-rate-limit";

// Default store (automatic)
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 100
});

// Explicit MemoryStore instance
const limiter2 = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 100,
  store: new MemoryStore()
});

MemoryStore Characteristics

  • Local to process: Data is not shared between server instances
  • Automatic cleanup: Expired entries are automatically removed
  • Memory efficient: Uses dual-map structure to minimize memory usage
  • No persistence: Data is lost when the process restarts
  • High performance: No network overhead for storage operations

Store Interface

For custom storage backends, implement the Store interface:

interface Store {
  // Optional: Initialize store with middleware options
  init?(options: Options): void;
  
  // Optional: Retrieve current hit count and reset time
  get?(key: string): Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
  
  // Required: Increment hit count for a key
  increment(key: string): Promise<ClientRateLimitInfo> | ClientRateLimitInfo;
  
  // Required: Decrement hit count for a key
  decrement(key: string): Promise<void> | void;
  
  // Required: Reset hit count for a key
  resetKey(key: string): Promise<void> | void;
  
  // Optional: Reset all keys
  resetAll?(): Promise<void> | void;
  
  // Optional: Cleanup and shutdown
  shutdown?(): Promise<void> | void;
  
  // Optional: Indicates if keys are local to this instance
  localKeys?: boolean;
  
  // Optional: Key prefix for multi-tenant scenarios
  prefix?: string;
}

interface Options {
  windowMs: number;
  limit: number | ValueDeterminingMiddleware<number>;
  // ... other configuration options
}

Custom Store Implementation

Example Redis-based store implementation:

import Redis from "ioredis";

class RedisStore implements Store {
  private client: Redis;
  private windowMs: number = 60000;
  public localKeys = false; // Data is shared across instances
  public prefix = "rl:";
  
  constructor(client: Redis) {
    this.client = client;
  }
  
  init(options: Options): void {
    this.windowMs = options.windowMs;
  }
  
  async get(key: string): Promise<ClientRateLimitInfo | undefined> {
    const multi = this.client.multi();
    multi.get(`${this.prefix}${key}:hits`);
    multi.pttl(`${this.prefix}${key}:hits`);
    
    const results = await multi.exec();
    if (!results || !results[0] || !results[0][1]) return undefined;
    
    const totalHits = parseInt(results[0][1] as string, 10);
    const ttl = results[1] ? results[1][1] as number : -1;
    const resetTime = ttl > 0 ? new Date(Date.now() + ttl) : undefined;
    
    return { totalHits, resetTime };
  }
  
  async increment(key: string): Promise<ClientRateLimitInfo> {
    const multi = this.client.multi();
    const redisKey = `${this.prefix}${key}:hits`;
    
    multi.incr(redisKey);
    multi.pexpire(redisKey, this.windowMs);
    multi.pttl(redisKey);
    
    const results = await multi.exec();
    const totalHits = results![0][1] as number;
    const ttl = results![2][1] as number;
    const resetTime = ttl > 0 ? new Date(Date.now() + ttl) : undefined;
    
    return { totalHits, resetTime };
  }
  
  async decrement(key: string): Promise<void> {
    const redisKey = `${this.prefix}${key}:hits`;
    const current = await this.client.get(redisKey);
    if (current && parseInt(current, 10) > 0) {
      await this.client.decr(redisKey);
    }
  }
  
  async resetKey(key: string): Promise<void> {
    await this.client.del(`${this.prefix}${key}:hits`);
  }
  
  async resetAll(): Promise<void> {
    const keys = await this.client.keys(`${this.prefix}*:hits`);
    if (keys.length > 0) {
      await this.client.del(...keys);
    }
  }
  
  async shutdown(): Promise<void> {
    await this.client.disconnect();
  }
}

// Usage
const redis = new Redis({ host: "localhost", port: 6379 });
const limiter = rateLimit({
  store: new RedisStore(redis),
  windowMs: 15 * 60 * 1000,
  limit: 100
});

Legacy Store Interface

For backward compatibility with older store implementations:

interface LegacyStore {
  // Callback-based increment method
  incr(key: string, callback: IncrementCallback): void;
  
  // Synchronous decrement method  
  decrement(key: string): void;
  
  // Synchronous reset method
  resetKey(key: string): void;
  
  // Optional reset all method
  resetAll?(): void;
}

type IncrementCallback = (
  error: Error | undefined,
  totalHits: number,
  resetTime: Date | undefined,
) => void;

Legacy stores are automatically wrapped to work with the modern async interface.

Store Selection Guidelines

Use MemoryStore when:

  • Single server deployment
  • High performance requirements
  • Simple setup needed
  • Data persistence not required

Use External Store when:

  • Multiple server instances (load balancing)
  • Data persistence required
  • Shared rate limiting across services
  • Advanced storage features needed (clustering, replication)

Store Configuration Examples

Multiple Rate Limiters with Shared Store

const sharedStore = new RedisStore(redisClient);

const apiLimiter = rateLimit({
  store: sharedStore,
  windowMs: 15 * 60 * 1000,
  limit: 1000
});

const authLimiter = rateLimit({
  store: sharedStore, // Same store, different keys
  windowMs: 15 * 60 * 1000,
  limit: 5,
  keyGenerator: (req) => `auth:${req.ip}`
});

Store with Prefix for Multi-tenant

class PrefixedRedisStore extends RedisStore {
  constructor(client: Redis, prefix: string) {
    super(client);
    this.prefix = prefix;
  }
}

const tenantALimiter = rateLimit({
  store: new PrefixedRedisStore(redis, "tenant-a:"),
  windowMs: 15 * 60 * 1000,
  limit: 1000
});

const tenantBLimiter = rateLimit({
  store: new PrefixedRedisStore(redis, "tenant-b:"),
  windowMs: 15 * 60 * 1000, 
  limit: 500
});

Store Error Handling

const limiter = rateLimit({
  store: externalStore,
  passOnStoreError: true, // Allow requests if store fails
  windowMs: 15 * 60 * 1000,
  limit: 100
});

// Monitor store errors
externalStore.on?.('error', (error) => {
  console.error('Rate limit store error:', error);
  // Implement fallback logic or alerting
});

Performance Considerations

MemoryStore Performance

  • Memory usage: Approximately 100-200 bytes per unique key
  • Cleanup frequency: Every windowMs milliseconds
  • Concurrency: Thread-safe for Node.js single-threaded model

External Store Performance

  • Network latency: Adds ~1-5ms per request
  • Connection pooling: Reuse connections for better performance
  • Batch operations: Use multi/pipeline operations when possible
  • Error handling: Configure appropriate timeouts and retries

Store Monitoring

// Add monitoring to custom stores
class MonitoredRedisStore extends RedisStore {
  async increment(key: string): Promise<ClientRateLimitInfo> {
    const start = Date.now();
    try {
      const result = await super.increment(key);
      console.log(`Store increment took ${Date.now() - start}ms`);
      return result;
    } catch (error) {
      console.error('Store increment failed:', error);
      throw error;
    }
  }
}