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:
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:
{
"error": "Internal Server Error",
"message": "ERR connection refused"
}Redis connection error logs:
[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: truein 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: trueto not count errored requests against the limit