This document covers storage options for express-rate-limit, including the built-in MemoryStore and the interface for creating custom storage backends.
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;
}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()
});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
}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
});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.
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}`
});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
});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
});windowMs milliseconds// 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;
}
}
}