January 9, 2025

ioredis Debugging Tips: From CLI to GUI

CorbinCorbin

ioredis is the go-to Redis client for Node.js. Great TypeScript support, built-in cluster handling, Lua scripting, streams - it does everything.

But when things go wrong, debugging can feel like guessing. Connection timeouts. Commands hanging. Memory leaks. Is it ioredis? Redis? Your code?

Here's how to figure it out.

Enable Debug Mode

First step: see what ioredis is actually doing.

// Set before requiring ioredis
process.env.DEBUG = 'ioredis:*';

const Redis = require('ioredis');
const redis = new Redis();

Or from command line:

DEBUG=ioredis:* node app.js

Output shows every command, connection event, and internal state change. Verbose but useful for connection issues.

Connection Problems

Symptom: Commands Hang Forever

await redis.get('key'); // Never resolves

Possible causes:

  1. Not connected yet

ioredis queues commands until connected. If connection never establishes, commands wait forever.

const redis = new Redis({
  host: 'redis.example.com',
  port: 6379,
  connectTimeout: 10000,  // Fail fast
  maxRetriesPerRequest: 3,
});

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

redis.on('connect', () => {
  console.log('Connected to Redis');
});
  1. Offline queue enabled

By default, ioredis queues commands when disconnected:

const redis = new Redis({
  enableOfflineQueue: false,  // Fail immediately when disconnected
});
  1. Cluster node down

In cluster mode, commands to a down node hang until failover.

const cluster = new Redis.Cluster([...], {
  clusterRetryStrategy: (times) => {
    if (times > 3) return null;  // Stop retrying
    return Math.min(times * 100, 3000);
  },
});

Symptom: Frequent Reconnections

Check your Redis server's timeout setting:

redis-cli CONFIG GET timeout

If set to something like 300 seconds, idle connections get closed. ioredis reconnects, but frequent reconnects waste resources.

Fix: Enable keepalive

const redis = new Redis({
  keepAlive: 30000,  // Send keepalive every 30s
});

Memory Issues

Symptom: Node.js Memory Growing

Your Node process memory keeps increasing. Suspecting Redis.

Check 1: Large values in responses

// Bad - loading huge value into memory
const bigValue = await redis.get('giant-json-blob');

// Better - stream it
const stream = redis.scanStream({ match: 'cache:*' });
stream.on('data', (keys) => {
  // Process in chunks
});

Check 2: Subscriber leaks

Each subscription keeps a connection open:

// Creating new subscriber each request = memory leak
app.get('/subscribe', (req, res) => {
  const sub = new Redis();  // New connection!
  sub.subscribe('channel');
  // ...
});

// Better - reuse subscriber
const subscriber = new Redis();
subscriber.subscribe('channel');

Check 3: Pipeline/transaction not executed

const pipeline = redis.pipeline();
pipeline.get('key1');
pipeline.get('key2');
// Forgot to exec()! Pipeline stays in memory
await pipeline.exec();  // Don't forget this

Command-Level Debugging

See Actual Commands Sent

redis.on('commandSent', (command) => {
  console.log('Sent:', command.name, command.args);
});

Monitor All Commands (Development Only)

redis.monitor((err, monitor) => {
  monitor.on('monitor', (time, args) => {
    console.log(time, args);
  });
});

Warning: MONITOR is expensive. Never in production.

Check Server-Side Slow Log

Commands that took too long on the Redis server:

const slowlog = await redis.slowlog('GET', 10);
console.log(slowlog);

Cluster-Specific Issues

Symptom: MOVED Errors

ReplyError: MOVED 12345 192.168.1.5:6379

Cluster topology changed. ioredis usually handles this automatically, but if you see it:

const cluster = new Redis.Cluster([...], {
  redisOptions: {
    // Refresh cluster slots periodically
  },
  clusterRetryStrategy: (times) => Math.min(times * 100, 3000),
});

// Force slot refresh
await cluster.cluster('SLOTS');

Symptom: Some Keys Unreachable

Cluster mode routes by key slot. If a node is down:

// Check which node handles a key
const slot = cluster.slots[cluster.keySlot('mykey')];
console.log('Key handled by:', slot);

Lua Script Debugging

Symptom: EVALSHA Returns NOSCRIPT

Script not loaded on target server:

// Define script once
const myScript = redis.defineCommand('mycommand', {
  numberOfKeys: 1,
  lua: `return redis.call('GET', KEYS[1])`,
});

// ioredis auto-loads script when needed
await redis.mycommand('key');

If using raw EVALSHA:

try {
  await redis.evalsha(sha, 1, 'key');
} catch (e) {
  if (e.message.includes('NOSCRIPT')) {
    // Fallback to EVAL
    await redis.eval(script, 1, 'key');
  }
}

Debugging Lua Scripts

Test in CLI first:

redis-cli EVAL "return redis.call('GET', KEYS[1])" 1 mykey

Add debug logging:

-- In your script
redis.log(redis.LOG_WARNING, "Debug: " .. tostring(ARGV[1]))

Check Redis logs for output.

Visual Debugging with Redimo

Sometimes you need to see the data, not just the commands.

Verify Data Wrote Correctly

await redis.hset('user:1001', 'name', 'John', 'email', 'john@example.com');
// Did it work? What's actually stored?

In Redimo: Navigate to user:1001, see the hash fields and values. No guessing.

Check Key Expiration

await redis.set('session:abc', 'data', 'EX', 3600);
// Is TTL actually set?

Redimo shows TTL countdown on every key. Catch missing expirations before they become memory leaks.

Investigate Patterns

// Your code creates these keys
await redis.set('cache:user:1001:profile', '...');
await redis.set('cache:user:1001:settings', '...');
await redis.set('cache:api:response:abc', '...');

Pattern Monitor on cache:* shows all cached keys, their types, sizes, and TTLs. Spot anomalies.

Debug Complex Data Structures

await redis.xadd('stream:events', '*', 'type', 'click', 'data', JSON.stringify({...}));

Streams in CLI are hard to read. Redimo renders them as a timeline view.

Common Mistakes

1. Not Handling Errors

// Bad - silent failures
redis.get('key');

// Good - handle errors
try {
  await redis.get('key');
} catch (e) {
  console.error('Redis error:', e);
}

// Or with callbacks
redis.get('key', (err, result) => {
  if (err) console.error(err);
});

2. Blocking Operations

// Bad - KEYS blocks
const keys = await redis.keys('pattern:*');

// Good - SCAN doesn't block
const stream = redis.scanStream({ match: 'pattern:*' });

3. Not Closing Connections

// In tests or scripts
afterAll(async () => {
  await redis.quit();
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await redis.quit();
  process.exit(0);
});

4. Wrong Serialization

// Bad - stores "[object Object]"
await redis.set('data', {foo: 'bar'});

// Good - serialize
await redis.set('data', JSON.stringify({foo: 'bar'}));

// Reading back
const data = JSON.parse(await redis.get('data'));

Quick Debugging Checklist

Symptom Check
Commands hang Connection state, offline queue, timeout
Memory growth Large values, subscriber leaks, unflushed pipelines
Intermittent failures Cluster topology, reconnection settings
Wrong data Serialization, key names, data types
Slow commands Slow log, MONITOR, command complexity

Useful ioredis Config

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  
  // Connection
  connectTimeout: 10000,
  keepAlive: 30000,
  
  // Reliability
  maxRetriesPerRequest: 3,
  enableOfflineQueue: false,  // Fail fast
  
  // Debugging
  showFriendlyErrorStack: true,  // Better error traces
});

// Event handlers
redis.on('error', console.error);
redis.on('connect', () => console.log('Connected'));
redis.on('reconnecting', () => console.log('Reconnecting...'));

ioredis gives you full control. With debug mode, events, and visual tools, you can diagnose any issue.

Download Redimo to see your Redis data alongside your ioredis debugging.

Ready for Download

Try Redimo Today

Pattern Monitor, CRUD operations, SSH Tunneling.
Everything you need to manage Redis at light speed.

macOS & Windows