Introduction

Express rate limiting with a Redis store provides distributed rate limiting across multiple application instances. When the Redis connection is lost -- due to Redis restart, network partition, or Redis reaching its max client limit -- the rate limiter either blocks all requests (fail-closed) or allows unlimited requests (fail-open), depending on configuration. The default behavior of most rate limiting libraries is to reject requests when Redis is unavailable, causing a complete service outage for all users even though the application itself is healthy.

Symptoms

All requests blocked when Redis is down:

bash
Error: Redis connection lost
    at RedisStore.increment (/app/node_modules/express-rate-limit/dist/index.js:123:15)
    at rateLimit (/app/node_modules/express-rate-limit/dist/index.js:89:23)

Or 500 errors:

json
{
  "error": "Internal Server Error",
  "message": "ERR connection refused"
}

Redis connection error logs:

bash
[ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1146:16)

Common Causes

  • Redis server not running: Redis process crashed or was not started
  • Network partition between app and Redis: Firewall change, VPC routing issue
  • Redis maxclients reached: Too many connections from other services
  • Redis AUTH failure: Password changed but not updated in application config
  • Rate limiter does not handle Redis errors: Library throws instead of gracefully degrading
  • No reconnection configured: Redis client does not automatically reconnect

Step-by-Step Fix

Step 1: Configure Redis client with reconnection

```javascript const Redis = require('ioredis'); const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis');

const redisClient = new Redis({ host: process.env.REDIS_HOST || '127.0.0.1', port: process.env.REDIS_PORT || 6379, password: process.env.REDIS_PASSWORD || undefined, maxRetriesPerRequest: 3, retryStrategy(times) { const delay = Math.min(times * 50, 2000); return delay; // Reconnect with exponential backoff }, enableOfflineQueue: true, // Queue commands while disconnected });

redisClient.on('error', (err) => { console.error('Redis error:', err.message); });

redisClient.on('connect', () => { console.log('Redis connected'); });

redisClient.on('reconnecting', () => { console.log('Redis reconnecting...'); }); ```

Step 2: Configure rate limiter with fallback

```javascript // In-memory fallback store const memoryStore = new Map();

const fallbackStore = { async increment(key) { const entry = memoryStore.get(key) || { count: 0, resetTime: Date.now() + 60000 }; entry.count++; memoryStore.set(key, entry); return entry; }, async decrement(key) { const entry = memoryStore.get(key); if (entry) entry.count = Math.max(0, entry.count - 1); }, async resetKey(key) { memoryStore.delete(key); }, };

// Wrap the Redis store with fallback const createStore = () => { let useFallback = false;

const redisStore = new RedisStore({ sendCommand: (...args) => redisClient.call(...args), });

return { async increment(key) { if (useFallback) { return fallbackStore.increment(key); } try { return await redisStore.increment(key); } catch (err) { console.warn('Redis rate limit failed, using fallback:', err.message); useFallback = true;

// Try to recover Redis connection setTimeout(() => { useFallback = false; }, 30000);

return fallbackStore.increment(key); } }, async decrement(key) { if (useFallback) { return fallbackStore.decrement(key); } return redisStore.decrement(key); }, async resetKey(key) { if (useFallback) { return fallbackStore.resetKey(key); } return redisStore.resetKey(key); }, }; };

const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window store: createStore(), message: { error: 'Too many requests, please try again later' }, standardHeaders: true, legacyHeaders: false, });

app.use('/api/', limiter); ```

Prevention

  • Configure Redis client with automatic reconnection and exponential backoff
  • Implement an in-memory fallback store for when Redis is unavailable
  • Set enableOfflineQueue: true in ioredis to queue commands during disconnection
  • Monitor Redis connection status and alert on prolonged disconnections
  • Add Redis health checks to your application's health endpoint
  • Use Redis Sentinel or Redis Cluster for high availability
  • Set rate limit skipFailedRequests: true to not count errored requests against the limit