Getting Redis + Node.js to work properly

Redis Logo

Redis has been the standard for caching for years. It works great until it doesn't, and then you're debugging at 2am wondering why your app is throwing weird connection errors.

The node-redis client has improved a lot, especially since v4. The Redis documentation covers the basics but somehow misses all the stuff that breaks in production.

Basic connection setup

I'm running node-redis v4.something - way better than the callback nightmare of v3. Still has its moments though.

import { createClient } from 'redis';

const client = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379',
  socket: {
    connectTimeout: 10000,   // Default timeout killed us in AWS
    keepAlive: true,         
    family: 4               // Docker + IPv6 = bad time
  }
});

client.on('error', (err) => {
  console.error('Redis connection failed:', err);
});

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

await client.connect();

That 10 second timeout saved me during our last deployment. All connections were timing out with the default setting - network was just slightly slower than expected. Bumped it up and the errors stopped. Might have been coincidence but I'm not changing it back.

But getting one connection to work is just the beginning. Once you have multiple users hitting your app simultaneously, that single connection becomes a bottleneck. Connection pooling becomes critical.

Connection pools (because one connection isn't enough)

Node.js Logo

Connection pools sound optional until you get real traffic and everything falls apart. One connection works fine for local development, but we started getting ECONNRESET errors once we had like 30 concurrent users.

// This kills performance under load
const sharedClient = createClient({ url: process.env.REDIS_URL });

// Better - use a proper pool library like generic-pool
import genericPool from 'generic-pool';

const pool = genericPool.createPool({
  create: () => {
    const client = createClient({ url: process.env.REDIS_URL });
    return client.connect().then(() => client);
  },
  destroy: (client) => client.quit()
}, {
  min: 2,
  max: 8
});

// Get a connection when needed
const client = await pool.acquire();
try {
  const result = await client.get('some-key');
  return result;
} finally {
  pool.release(client);
}

Without pooling, you're creating new connections for every request. Works fine until traffic picks up. Generic-pool is what I use for this.

Security basics

Redis security was terrible in early versions - no auth by default. Now you can use ACLs and TLS, but lots of people still run Redis with default settings. The Redis security checklist covers the basics.

const secureClient = createClient({
  username: 'app-user',    
  password: process.env.REDIS_PASSWORD,
  socket: {
    tls: true,             
    rejectUnauthorized: true
  }
});

Create a dedicated user for your app with minimal permissions. The default Redis user has admin access which you don't want.

Security matters, but so does availability. If your single Redis instance can't handle the load, you'll need to think about horizontal scaling.

Clustering (when one Redis isn't enough)

Redis Cluster Architecture

Redis clustering splits your data across multiple Redis instances. Sounds great until you realize it's eventually consistent, which means your data might be slightly out of sync between nodes.

const cluster = createCluster({
  rootNodes: [
    { host: 'redis-1.internal', port: 6379 },
    { host: 'redis-2.internal', port: 6379 },
    { host: 'redis-3.internal', port: 6379 }
  ]
});

cluster.on('nodeError', (error, address) => {
  console.error(`Node ${address} is being a problem:`, error);
});

Redis cluster doesn't guarantee strong consistency. Data might be slightly different between nodes, especially during network partitions. Spent one late night debugging disappearing user sessions - turned out the cluster was rebalancing during a network issue. Session got written to one node but another node didn't see it for 30+ seconds.

Pipelining

Node-redis v4+ does automatic pipelining, which batches commands sent in the same tick. Actually works well and makes things faster.

// These get batched automatically
const [user, session] = await Promise.all([
  client.get('user:123'),
  client.hGetAll('session:abc')
]);

One of those features that actually works as advertised.

Handling Redis failures

Redis will fail. Network issues happen. When Redis is down, don't let it take your entire app with it.

async function getCachedData(key) {
  try {
    return await client.get(key);
  } catch (error) {
    console.warn('Redis is being difficult, falling back to database');
    return await database.get(key);  // Slower but reliable
  }
}

Always have a fallback. Cache failures should slow things down, not break everything.

Advanced Redis features

RedisInsight Interface

Beyond basic caching, Redis has some advanced features that can be really useful or cause headaches. Here's what matters in production.

Distributed locking

Distributed locking prevents race conditions when multiple app instances are running. Without it, you might process payments twice or send duplicate emails. Happens more than you'd think.

// Simple lock that works
async function withLock(key, callback, timeout = 10000) {
  const lockKey = `lock:${key}`;
  const acquired = await client.set(lockKey, '1', { NX: true, PX: timeout });
  
  if (acquired !== 'OK') {
    throw new Error('Could not acquire lock');
  }
  
  try {
    return await callback();
  } finally {
    await client.del(lockKey);
  }
}

// Use it like this
await withLock('payment:123', async () => {
  // Only one server can run this at a time
  return processPayment(123);
});

That timeout is crucial. Without it, crashed processes leave locks forever. Found this out the hard way when our payment processor died and left a bunch of orders stuck because the lock never expired. Customer support was not happy. There's fancier stuff like Redlock but honestly I've never needed it.

Pub/Sub messaging

Redis pub/sub works well for simple messaging between services. Just remember that if a service is down when you send a message, that message disappears forever. Messages get lost if nobody's listening.

// Publisher (sends messages)
await publisher.publish('user-events', JSON.stringify({
  type: 'user_registered',
  userId: 123,
  email: 'user@example.com'
}));

// Subscriber (receives messages) - needs separate connection
subscriber.on('message', (channel, message) => {
  const event = JSON.parse(message);
  if (event.type === 'user_registered') {
    sendWelcomeEmail(event.email);
  }
});

await subscriber.subscribe('user-events');

Keep it simple. If you need guaranteed delivery, use Redis Streams instead of pub/sub, or consider message queues like RabbitMQ or Apache Kafka.

Redis Streams

Redis Streams are like pub/sub but messages don't disappear if nobody's listening. More reliable but more complex to set up. Similar to Apache Kafka but built into Redis.

// Add to stream
await client.xAdd('events', '*', {
  userId: '123',
  action: 'login',
  timestamp: Date.now().toString()
});

// Read from stream (consumer group)
const messages = await client.xReadGroup(
  'mygroup', 'consumer1',
  [{ key: 'events', id: '>' }],
  { COUNT: 10, BLOCK: 1000 }
);

Streams are worth it if you need guaranteed message processing, but the setup is more involved than pub/sub. The Redis Streams tutorial explains the concepts better than most documentation.

Session storage

Redis Data Structures Overview

Redis works well for session storage - it's fast and all your app instances can access the same session data. Better than storing sessions in a database.

// Simple session storage
const sessionId = crypto.randomUUID();

// Store session data with 24 hour expiration
await client.hSet(`session:${sessionId}`, {
  userId: '123',
  createdAt: Date.now().toString(),
  lastSeen: Date.now().toString()
});
await client.expire(`session:${sessionId}`, 86400);

// Get session data
const session = await client.hGetAll(`session:${sessionId}`);

Use Redis hashes for sessions - you can store multiple fields and update them individually. More efficient than storing JSON strings.

That covers the main Redis features you'll actually use in production.

Which Redis client should you actually use?

Client

My Take

When to Use

When to Avoid

node-redis

Official client, decent docs

New projects, need Redis Stack

If ioredis works fine for you

ioredis

Better error handling

Existing projects, cluster setups

If you need Redis Stack features

redis-om-node

Nice for prototypes, slower in prod

Rapid prototyping, search apps

High performance needs

Bull

Solid for job processing

Background jobs

Simple caching

Questions people actually ask

Q

Redis keeps disconnecting, what the hell?

A

You're probably seeing ECONNRESET or Socket connection failed errors. Your connection timeout is too short. Set it to 10 seconds:

const client = createClient({
  socket: { connectTimeout: 10000 }
});

Also make sure you're handling errors or your app will crash:

client.on('error', (err) => {
  console.error('Redis error:', err);
  // Don't let this kill your app
});
Q

Should I use node-redis v5 or stick with ioredis?

A

If ioredis is working for you, don't change it. Node-redis v5 has automatic pipelining which is nice, but ioredis has better error handling. Only switch if you need Redis Stack features.

Q

My Redis memory usage keeps growing, what's wrong?

A

You're probably not setting TTL on your keys. Always set an expiration:

// Wrong - no expiration
await client.set('user:123', userData);

// Right - expires in 1 hour
await client.setEx('user:123', 3600, userData);

Check your keys with redis-cli and look for ones without TTL. Those are your memory leaks. Use TTL keyname to check if a key expires - returns -1 if it doesn't.

Q

Can I store JSON data in Redis?

A

Yes, just stringify it:

await client.set('user:123', JSON.stringify({ name: 'John', age: 30 }));
const user = JSON.parse(await client.get('user:123'));

If you have Redis Stack, you can use the JSON module, but plain strings work fine for most cases.

Q

How do I handle Redis being down?

A

Have a fallback and don't let Redis failures kill your app:

async function getUser(id) {
  try {
    const cached = await client.get(`user:${id}`);
    if (cached) return JSON.parse(cached);
  } catch (error) {
    console.warn('Redis is down, falling back to database');
  }
  
  // Fallback to database
  const user = await database.getUser(id);
  
  // Try to cache for next time (but don't fail if Redis is still down)
  try {
    await client.setEx(`user:${id}`, 3600, JSON.stringify(user));
  } catch (error) {
    // Redis still down, ignore
  }
  
  return user;
}
Q

When should I use Redis Cluster?

A

When a single Redis instance can't handle your load anymore. But clustering is complex

  • only do it if you really need horizontal scaling. Try throwing more RAM at the problem first, it's usually cheaper.
Q

Is Redis fast enough for real-time features?

A

Yes, Redis is fast. But your network latency matters more than Redis performance. If you're getting slow responses, check your network first.

Q

How do I debug Redis performance issues?

A

Use redis-cli --latency-history to check if Redis itself is slow. If it's not Redis, it's probably your network or connection pooling. Also try SLOWLOG GET 10 to see slow queries.

Q

Should I use Redis for sessions?

A

Yes, Redis is perfect for sessions. Use hashes for complex session data:

await client.hSet(`session:${sessionId}`, {
  userId: '123',
  lastSeen: Date.now().toString(),
  permissions: JSON.stringify(['read', 'write'])
});
Q

Why does my app crash when Redis restarts?

A

Because you're not handling connection errors. Add error handlers and Redis will reconnect automatically:

client.on('error', (err) => {
  console.error('Redis connection error:', err);
  // Don't exit the process
});

client.on('reconnecting', () => {
  console.log('Redis reconnecting...');
});
Q

Getting weird "redis.set is not a function" errors?

A

You forgot to await the client connection. This will bite you if you're not careful:

import { createClient } from 'redis';

const client = createClient();
await client.set('key', 'value'); // Error: client.set is not a function

// Fix: Connect first
await client.connect(); 
await client.set('key', 'value'); // Works now

Related Tools & Recommendations

compare
Similar content

Bun vs Node.js vs Deno: JavaScript Runtime Performance Comparison

Three weeks of testing revealed which JavaScript runtime is actually faster (and when it matters)

Bun
/compare/bun/node.js/deno/performance-comparison
100%
compare
Similar content

Redis vs Memcached vs Hazelcast: Caching Decision Guide

Three caching solutions that tackle fundamentally different problems. Redis 8.2.1 delivers multi-structure data operations with memory complexity. Memcached 1.6

Redis
/compare/redis/memcached/hazelcast/comprehensive-comparison
99%
compare
Recommended

Python vs JavaScript vs Go vs Rust - Production Reality Check

What Actually Happens When You Ship Code With These Languages

go
/compare/python-javascript-go-rust/production-reality-check
93%
integration
Similar content

Claude API Node.js Express: Advanced Code Execution & Tools Guide

Build production-ready applications with Claude's code execution and file processing tools

Claude API
/integration/claude-api-nodejs-express/advanced-tools-integration
83%
news
Recommended

Google Avoids Breakup, Stock Surges

Judge blocks DOJ breakup plan. Google keeps Chrome and Android.

go
/news/2025-09-04/google-antitrust-chrome-victory
66%
troubleshoot
Recommended

Docker Desktop Won't Install? Welcome to Hell

When the "simple" installer turns your weekend into a debugging nightmare

Docker Desktop
/troubleshoot/docker-cve-2025-9074/installation-startup-failures
60%
howto
Recommended

Complete Guide to Setting Up Microservices with Docker and Kubernetes (2025)

Split Your Monolith Into Services That Will Break in New and Exciting Ways

Docker
/howto/setup-microservices-docker-kubernetes/complete-setup-guide
60%
troubleshoot
Recommended

Fix Docker Daemon Connection Failures

When Docker decides to fuck you over at 2 AM

Docker Engine
/troubleshoot/docker-error-during-connect-daemon-not-running/daemon-connection-failures
60%
integration
Recommended

OpenTelemetry + Jaeger + Grafana on Kubernetes - The Stack That Actually Works

Stop flying blind in production microservices

OpenTelemetry
/integration/opentelemetry-jaeger-grafana-kubernetes/complete-observability-stack
58%
troubleshoot
Recommended

Fix Kubernetes ImagePullBackOff Error - The Complete Battle-Tested Guide

From "Pod stuck in ImagePullBackOff" to "Problem solved in 90 seconds"

Kubernetes
/troubleshoot/kubernetes-imagepullbackoff/comprehensive-troubleshooting-guide
58%
howto
Recommended

Lock Down Your K8s Cluster Before It Costs You $50k

Stop getting paged at 3am because someone turned your cluster into a bitcoin miner

Kubernetes
/howto/setup-kubernetes-production-security/hardening-production-clusters
58%
tool
Similar content

Redis Cluster Production Issues: Troubleshooting & Survival Guide

When Redis clustering goes sideways at 3AM and your boss is calling. The essential troubleshooting guide for split-brain scenarios, slot migration failures, and

Redis
/tool/redis/clustering-production-issues
56%
pricing
Recommended

Got Hit With a $3k Vercel Bill Last Month: Real Platform Costs

These platforms will fuck your budget when you least expect it

Vercel
/pricing/vercel-vs-netlify-vs-cloudflare-pages/complete-pricing-breakdown
53%
news
Recommended

Google Avoids $2.5 Trillion Breakup in Landmark Antitrust Victory

Federal judge rejects Chrome browser sale but bans exclusive search deals in major Big Tech ruling

OpenAI/ChatGPT
/news/2025-09-05/google-antitrust-victory
50%
tool
Similar content

Redis Overview: In-Memory Database, Caching & Getting Started

The world's fastest in-memory database, providing cloud and on-premises solutions for caching, vector search, and NoSQL databases that seamlessly fit into any t

Redis
/tool/redis/overview
44%
integration
Similar content

Django Celery Redis Docker: Fix Broken Background Tasks & Scale Production

Master Django, Celery, Redis, and Docker for robust distributed task queues. Fix common issues, optimize Docker Compose, and deploy scalable background tasks in

Redis
/integration/redis-django-celery-docker/distributed-task-queue-architecture
44%
tool
Similar content

Node.js Microservices: Avoid Pitfalls & Build Robust Systems

Learn why Node.js microservices projects often fail and discover practical strategies to build robust, scalable distributed systems. Avoid common pitfalls and e

Node.js
/tool/node.js/microservices-architecture
44%
troubleshoot
Similar content

Fix Redis ERR max clients reached: Solutions & Prevention

When Redis starts rejecting connections, you need fixes that work in minutes, not hours

Redis
/troubleshoot/redis/max-clients-error-solutions
41%
integration
Similar content

Kafka, Redis & RabbitMQ: Event Streaming Architecture Guide

Kafka + Redis + RabbitMQ Event Streaming Architecture

Apache Kafka
/integration/kafka-redis-rabbitmq/architecture-overview
41%
review
Similar content

Bun vs Node.js vs Deno: JavaScript Runtime Production Guide

Two years of runtime fuckery later, here's the truth nobody tells you

Bun
/review/bun-nodejs-deno-comparison/production-readiness-assessment
41%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization