# 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.

```typescript { .api }
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

```typescript
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:

```typescript { .api }
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:

```typescript
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:

```typescript { .api }
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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;
    }
  }
}
```