ioredis Debugging Tips: From CLI to GUI
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:
- 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');
});
- Offline queue enabled
By default, ioredis queues commands when disconnected:
const redis = new Redis({
enableOfflineQueue: false, // Fail immediately when disconnected
});
- 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.