As33
@periodic/
arsenic

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

typescript
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

typescript
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.

KEYSdangerous

Full 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.

FLUSHALLdangerous

Deletes every key in every database on the Redis instance. No confirmation, no undo.

Fix: Restrict via Redis ACLs. Use targeted DEL or UNLINK.

FLUSHDBdangerous

Deletes 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.

BLPOPblocking

Blocks until an element is available in one of the specified lists.

⚠️ Always set a timeout. Unbounded blocking exhausts your connection pool.

BRPOPblocking

Blocking right-side pop from a list.

⚠️ Use with timeout and dedicated connection pool.

BRPOPLPUSHblocking

Atomically pops from one list and pushes to another — blocks until source has data.

⚠️ Deprecated in Redis 6.2. Use BLMOVE instead.

BLMOVEblocking

Blocking 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.

Returns every field in a hash — O(N) with hash size.

Fix: Use HMGET with explicit fields, or HSCAN.

Returns all set members — unbounded on large sets.

Fix: Use SSCAN to iterate, or SRANDMEMBER for sampling.

LRANGEslow

Scans a range of list elements.

Fix: Use narrow ranges or cursor-based pagination.

SORTslow

Sorts list/set/sorted set — memory and CPU intensive.

Fix: Pre-sort at write time using sorted sets.

SCANslow

O(N) across full keyspace when iterated to completion.

Fix: Use COUNT hints; move full scans to background jobs.

SSCANslow

O(N) across all set members when iterated to completion.

Fix: Use SISMEMBER for lookups; SSCAN with COUNT hints in background jobs.

HSCANslow

O(N) across all hash fields when iterated to completion.

Fix: Use HGET/HMGET for targeted access; move full iterations to background jobs.

ZSCANslow

O(N) across all sorted set members when iterated to completion.

Fix: Use ZRANGE/ZRANGEBYSCORE for bounded queries; ZSCORE/ZRANK for point lookups.

SUNIONslow

Set union — O(N) across all input sets.

Fix: Cache results with SUNIONSTORE + EXPIRE.

SINTERslow

Set intersection — O(N×M) across input sets.

Fix: Cache with SINTERSTORE; place the smallest set first.

SDIFFslow

Set difference — O(N) across all input sets combined.

Fix: Cache with SDIFFSTORE; denormalise at write time for frequent diffs.

Same 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.

Same as SINTER but also writes result.

Fix: Schedule as background job; TTL the result key.

Same as SDIFF but also writes result.

Fix: Run in background workers; cache result with TTL.

ZRANGEslow

Sorted 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.

Sorted set range by score — linear in elements returned.

Fix: Narrow the score range; paginate with LIMIT offset count.

Sorted set range by lex order — linear in elements matched.

Fix: Use tight lex bounds; paginate with LIMIT; avoid open-ended ranges.

Reverse rank range on sorted set — linear in elements returned.

Fix: Specify tight rank bounds; paginate. Prefer ZRANGE ... REV LIMIT in Redis 6.2+.

Reverse score range on sorted set — linear in elements returned.

Fix: Always bound the score range; paginate with LIMIT offset count.

Sorted set intersection — expensive with large input sets.

Fix: Cache results; run in background workers.

Sorted set union — O(N) + O(M log M) where M is result size.

Fix: Cache with TTL; schedule as background job.

OBJECTslow

Inspects internal Redis metadata — overhead in tight loops.

Fix: Use only for diagnostics, not in hot paths.

WAITslow

Blocks 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.

MULTInormal

Opens 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.

EXECnormal

Executes 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

typescript
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);
    }
  },
});