Redis Adapter
The Redis adapter monitors every command via ioredis or node-redis, automatically classifying each into one of four categories: dangerous, blocking, slow, or normal. 32 commands are explicitly documented.
Setup
import Redis from 'ioredis';
import { createMonitor, redisAdapter, getRedisCommandInfo, SLOW_REDIS_COMMANDS } from '@periodic/arsenic';
const redis = new Redis({ host: process.env.REDIS_HOST, port: 6379 });
const monitor = createMonitor({
slowQueryThresholdMs: 50, // Stricter for Redis
exporter: (event) => {
const category = event.metadata?.commandCategory;
if (category === 'dangerous') {
// KEYS/FLUSHALL/FLUSHDB should never fire in production
sendToPagerDuty(event);
} else if (category === 'blocking' && event.slow) {
sendToSlack(event);
} else if (category === 'slow' && event.slow) {
logger.warn('redis.slow', event);
}
},
});
redisAdapter(monitor, redis);Command utilities
import { getRedisCommandInfo, REDIS_COMMAND_INFO, SLOW_REDIS_COMMANDS } from '@periodic/arsenic';
// Get info for a specific command
const info = getRedisCommandInfo('KEYS');
// { command: 'KEYS', category: 'dangerous', docs: 'https://arsenicdev.online/redis/keys' }
// Commands not in the explicit list default to 'normal' — no signal page, fallback URL
const info2 = getRedisCommandInfo('GET');
// { command: 'GET', category: 'normal', docs: 'https://arsenicdev.online/redis/get' }
// 'normal' category commands emit no warning/critical signals under standard usage
// Array of all slow command names
console.log(SLOW_REDIS_COMMANDS); // ['HGETALL', 'SMEMBERS', ...]🔴 Dangerous Commands (3)
These should never run in production. Arsenic always flags these regardless of duration.
KEYSdangerousFull keyspace scan — blocks ALL Redis operations while running. Stalls for seconds on large datasets.
Fix: Use SCAN with a cursor for non-blocking key enumeration.
FLUSHALLdangerousDeletes every key in every database on the Redis instance. No confirmation, no undo.
Fix: Restrict via Redis ACLs. Use targeted DEL or UNLINK.
FLUSHDBdangerousDeletes all keys in the currently selected database. Equally destructive within its scope.
Fix: Restrict via ACLs. Use selective key deletion instead.
Never use KEYS in production
KEYS performs a full keyspace scan and blocks ALL other Redis operations while running. On a database with millions of keys, this can stall Redis for seconds. Use SCAN with a cursor instead.⚠️ Blocking Commands (4)
Commands that block the calling client until data is available or a timeout expires. Valid for task queues, but require careful configuration.
BLPOPblockingBlocks until an element is available in one of the specified lists.
⚠️ Always set a timeout. Unbounded blocking exhausts your connection pool.
BRPOPLPUSHblockingAtomically pops from one list and pushes to another — blocks until source has data.
⚠️ Deprecated in Redis 6.2. Use BLMOVE instead.
BLMOVEblockingBlocking atomic move between lists. Replaces BRPOPLPUSH.
⚠️ Always specify a timeout to prevent indefinite blocking.
⏱️ Slow Commands (23)
O(N) or worse complexity. Safe in small datasets but can degrade under load.
HGETALLslowReturns every field in a hash — O(N) with hash size.
Fix: Use HMGET with explicit fields, or HSCAN.
SMEMBERSslowReturns all set members — unbounded on large sets.
Fix: Use SSCAN to iterate, or SRANDMEMBER for sampling.
SORTslowSorts list/set/sorted set — memory and CPU intensive.
Fix: Pre-sort at write time using sorted sets.
SCANslowO(N) across full keyspace when iterated to completion.
Fix: Use COUNT hints; move full scans to background jobs.
SSCANslowO(N) across all set members when iterated to completion.
Fix: Use SISMEMBER for lookups; SSCAN with COUNT hints in background jobs.
HSCANslowO(N) across all hash fields when iterated to completion.
Fix: Use HGET/HMGET for targeted access; move full iterations to background jobs.
ZSCANslowO(N) across all sorted set members when iterated to completion.
Fix: Use ZRANGE/ZRANGEBYSCORE for bounded queries; ZSCORE/ZRANK for point lookups.
SINTERslowSet intersection — O(N×M) across input sets.
Fix: Cache with SINTERSTORE; place the smallest set first.
SDIFFslowSet difference — O(N) across all input sets combined.
Fix: Cache with SDIFFSTORE; denormalise at write time for frequent diffs.
SUNIONSTOREslowSame as SUNION but also writes result — adds a write on top of O(N) computation.
Fix: Run as background job with TTL on destination key.
SINTERSTOREslowSame as SINTER but also writes result.
Fix: Schedule as background job; TTL the result key.
SDIFFSTOREslowSame as SDIFF but also writes result.
Fix: Run in background workers; cache result with TTL.
ZRANGEslowSorted set range by rank — linear in elements returned. ZRANGE 0 -1 fetches the entire set.
Fix: Paginate with explicit rank bounds or LIMIT offset count.
ZRANGEBYSCOREslowSorted set range by score — linear in elements returned.
Fix: Narrow the score range; paginate with LIMIT offset count.
ZRANGEBYLEXslowSorted set range by lex order — linear in elements matched.
Fix: Use tight lex bounds; paginate with LIMIT; avoid open-ended ranges.
ZREVRANGEslowReverse rank range on sorted set — linear in elements returned.
Fix: Specify tight rank bounds; paginate. Prefer ZRANGE ... REV LIMIT in Redis 6.2+.
ZREVRANGEBYSCOREslowReverse score range on sorted set — linear in elements returned.
Fix: Always bound the score range; paginate with LIMIT offset count.
ZINTERSTOREslowSorted set intersection — expensive with large input sets.
Fix: Cache results; run in background workers.
ZUNIONSTOREslowSorted set union — O(N) + O(M log M) where M is result size.
Fix: Cache with TTL; schedule as background job.
OBJECTslowInspects internal Redis metadata — overhead in tight loops.
Fix: Use only for diagnostics, not in hot paths.
WAITslowBlocks until replicas acknowledge writes — latency from replication lag.
Fix: Set reasonable timeout; use only for strong consistency.
🟢 Normal Commands (2)
Tracked by the adapter but emit no warning or critical signals under normal usage. Observe them in your event stream or build custom alerting if needed.
MULTInormalOpens a transaction block. O(1) itself, but the EXEC duration reflects all queued commands.
ℹ️ Always pair with error handling that calls DISCARD if EXEC is never reached.
EXECnormalExecutes queued commands atomically. Duration reflects cumulative cost of the full transaction.
ℹ️ Returns null if a WATCH-ed key was modified — always handle the null case.
Category-based alerting
import { SignalSeverity } from '@periodic/arsenic';
const monitor = createMonitor({
exporter: (event) => {
const category = event.metadata?.commandCategory;
switch (category) {
case 'dangerous':
sendToPagerDuty(event);
break;
case 'blocking':
if (event.slow) sendToSlack(event);
break;
case 'slow':
logger.warn('redis.slow_command', {
command: event.operation,
durationMs: event.durationMs,
docs: event.metadata?.commandDocs,
});
if (event.severity === SignalSeverity.CRITICAL) sendToSlack(event);
break;
default:
logger.info('redis.query', event);
}
},
});