Node.js에서 Redis 연결 풀 관리
Managing Redis connection pools in Node.js applications with proper error handling, retry logic, and connection lifecycle management.
Node.js에서 Redis 연결 풀 관리
Introduction
Redis is one of the most popular data stores for caching and session management in Node.js applications. However, managing Redis connections properly is crucial for performance and reliability. Poor connection management can lead to connection leaks, timeouts, and cascading failures.
Environment
node --version
# v20.11.0
npm list redis
# my-app@1.0.0
# +-- redis@4.6.12
redis-cli ping
# PONGProblem
Under high load, the application started experiencing Redis connection errors:
Error: Redis connection lost
at Socket. (node:internal/streams/readable:758:22)
Error: connect ETIMEDOUT 127.0.0.1:6379
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16) The issue was that each request was creating a new Redis connection instead of reusing connections.
// BAD: Creating new connection per request
const { createClient } = require('redis');
app.get('/data', async (req, res) => {
const client = createClient(); // New connection every time!
await client.connect();
const data = await client.get('key');
await client.disconnect(); // Forgetting this causes connection leaks
res.json({ data });
});Analysis
Redis connections are expensive to create. Each connection involves:
- TCP handshake
- Redis protocol negotiation
- Authentication (if configured)
- Database selection
The solution is to use connection pooling - maintain a pool of reusable connections.
The connection lifecycle:
Request → Acquire from pool → Execute command → Release to pool
↓ ↓
Pool empty? → Create new connection Pool full? → WaitSolution
Solution 1: Using ioredis with built-in pooling
const Redis = require('ioredis');
const redis = new Redis({
host: '127.0.0.1',
port: 6379,
password: process.env.REDIS_PASSWORD,
db: 0,
// Connection pool settings
enableReadyCheck: true,
maxRetriesPerRequest: 3,
retryDelayOnFailover: 300,
// Auto-resend unfulfilled commands
enableOfflineQueue: true,
// Lazy connect (don't connect until first command)
lazyConnect: true
});
// Handle connection events
redis.on('connect', () => console.log('Redis connected'));
redis.on('ready', () => console.log('Redis ready'));
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('close', () => console.log('Redis connection closed'));
module.exports = redis;Solution 2: Connection pool with node-redis
const { createClient } = require('redis');
class RedisPool {
constructor(options = {}) {
this.pool = [];
this.maxSize = options.maxSize || 10;
this.currentSize = 0;
this.waitQueue = [];
this.config = {
url: options.url || 'redis://localhost:6379',
password: options.password,
socket: {
reconnectStrategy: (retries) => {
if (retries > 10) {
return new Error('Retry limit reached');
}
return Math.min(retries * 100, 3000);
}
}
};
}
async acquire() {
// Return existing connection if available
if (this.pool.length > 0) {
return this.pool.pop();
}
// Create new connection if under limit
if (this.currentSize < this.maxSize) {
this.currentSize++;
const client = createClient(this.config);
client.on('error', (err) => {
console.error('Pool client error:', err);
});
await client.connect();
return client;
}
// Wait for available connection
return new Promise((resolve) => {
this.waitQueue.push(resolve);
});
}
async release(client) {
// Check if anyone is waiting for a connection
if (this.waitQueue.length > 0) {
const waiter = this.waitQueue.shift();
waiter(client);
return;
}
// Return to pool
this.pool.push(client);
}
async disconnect() {
const allClients = [...this.pool];
this.pool = [];
for (const client of allClients) {
await client.quit();
}
this.currentSize = 0;
}
}
module.exports = RedisPool;Solution 3: Usage in Express middleware
const RedisPool = require('./redis-pool');
const redisPool = new RedisPool({
url: 'redis://localhost:6379',
maxSize: 20
});
// Middleware to attach Redis client
app.use(async (req, res, next) => {
const client = await redisPool.acquire();
req.redis = client;
// Release connection when response finishes
res.on('finish', async () => {
await redisPool.release(req.redis);
});
next();
});
// Usage in routes
app.get('/cache/:key', async (req, res) => {
const data = await req.redis.get(req.params.key);
res.json({ data: data ? JSON.parse(data) : null });
});
app.post('/cache/:key', async (req, res) => {
await req.redis.set(
req.params.key,
JSON.stringify(req.body),
'EX',
3600
);
res.json({ success: true });
});Solution 4: Health check and monitoring
class RedisMonitor {
constructor(pool) {
this.pool = pool;
this.stats = {
totalConnections: 0,
activeConnections: 0,
errors: 0
};
}
async checkHealth() {
try {
const client = await this.pool.acquire();
await client.ping();
await this.pool.release(client);
return { status: 'healthy' };
} catch (err) {
return { status: 'unhealthy', error: err.message };
}
}
getStats() {
return {
...this.stats,
poolSize: this.pool.pool.length,
waitingClients: this.pool.waitQueue.length
};
}
}Lessons Learned
- Never create new Redis connections per request - Always use a connection pool
- Set appropriate pool sizes - Too many connections waste resources, too few cause timeouts
- Implement reconnection logic - Redis connections can drop unexpectedly
- Monitor connection health - Track connection counts and error rates
- Use
lazyConnectfor better startup performance - Connect on first use, not at import time
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.