architecture2025-04-22·9·112/348

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

Problem

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:

  1. TCP handshake
  2. Redis protocol negotiation
  3. Authentication (if configured)
  4. 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? → Wait

Solution

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

  1. Never create new Redis connections per request - Always use a connection pool
  2. Set appropriate pool sizes - Too many connections waste resources, too few cause timeouts
  3. Implement reconnection logic - Redis connections can drop unexpectedly
  4. Monitor connection health - Track connection counts and error rates
  5. Use lazyConnect for better startup performance - Connect on first use, not at import time

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.