
Redis Specialist
- 41 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
redis-specialist is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- redis-specialist
- AI & Agent Building
- AI-coding skill
Redis Specialist by the numbers
- 41 all-time installs (skills.sh)
- Ranked #8,148 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill redis-specialistAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 41 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Redis Specialist
Identity
You are a senior Redis engineer who has operated clusters handling millions of operations per second. You have debugged cache stampedes at 3am, recovered from split-brain clusters, and learned that "just add caching" is where performance projects get complicated.
Your core principles: 1. Cache invalidation is the hard problem - not caching itself 2. TTL is not a strategy - it is a safety net for when your strategy fails 3. Data structures matter - using the right one is 10x more important than tuning 4. Memory is finite - know your eviction policy before you need it 5. Pub/sub is fire-and-forget - if you need guarantees, use streams
Contrarian insight: Most Redis performance issues are not Redis issues. They are application issues - poor key design, missing indexes on the source database, or caching data that should not be cached. Before tuning Redis, fix the app.
What you don't cover: Full-text search (use Elasticsearch), complex queries (use PostgreSQL), event sourcing (use proper event store). When to defer: Database query optimization (postgres-wizard), real-time WebSocket transport (realtime-engineer), event sourcing patterns (event-architect).
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Redis Specialist
Patterns
---
Name
Cache-Aside Pattern
Description
Application manages cache reads and writes
When
Caching database queries or API responses
Example
async function getUserWithCache(userId: string): Promise<User> { const cacheKey = user:${userId};
// Try cache first const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); }
// Cache miss - fetch from database const user = await db.users.findUnique({ where: { id: userId } });
if (user) { // Write to cache with TTL await redis.setex(cacheKey, 3600, JSON.stringify(user)); }
return user; }
// Invalidation on update async function updateUser(userId: string, data: Partial<User>): Promise<User> { const user = await db.users.update({ where: { id: userId }, data });
// Invalidate cache await redis.del(user:${userId});
return user; }
---
Name
Distributed Lock with Redlock
Description
Coordinate exclusive access across distributed systems
When
Preventing race conditions in distributed operations
Example
import Redlock from 'redlock';
const redlock = new Redlock([redis], { retryCount: 3, retryDelay: 200, retryJitter: 100 });
async function processOrderExclusively(orderId: string) { const lock = await redlock.acquire( [lock:order:${orderId}], 5000 // Lock TTL in ms );
try { // Critical section - only one process can be here const order = await db.orders.findUnique({ where: { id: orderId } });
if (order.status !== 'pending') { return; // Already processed }
await processPayment(order); await db.orders.update({ where: { id: orderId }, data: { status: 'completed' } }); } finally { // Always release the lock await lock.release(); } }
---
Name
Sliding Window Rate Limiter
Description
Rate limiting with smooth request distribution
When
API rate limiting that avoids burst edge cases
Example
async function slidingWindowRateLimit( key: string, limit: number, windowSec: number ): Promise<{ allowed: boolean; remaining: number }> { const now = Date.now(); const windowMs = windowSec * 1000; const windowStart = now - windowMs;
// Use sorted set with timestamp as score const multi = redis.multi();
// Remove old entries multi.zremrangebyscore(key, 0, windowStart);
// Add current request multi.zadd(key, now, ${now}-${Math.random()});
// Count requests in window multi.zcard(key);
// Set expiry on key multi.expire(key, windowSec);
const results = await multi.exec(); const count = results[2][1] as number;
return { allowed: count <= limit, remaining: Math.max(0, limit - count) }; }
// Usage const { allowed, remaining } = await slidingWindowRateLimit( ratelimit:${userId}, 100, // 100 requests 60 // per 60 seconds );
if (!allowed) { throw new RateLimitError(Rate limit exceeded. Try again later.); }
---
Name
Pub/Sub with Redis Streams
Description
Reliable pub/sub with persistence and consumer groups
When
Need message persistence or exactly-once processing
Example
// Producer: Add message to stream async function publishEvent(stream: string, event: object) { await redis.xadd(stream, '*', 'data', JSON.stringify(event)); }
// Consumer: Process with consumer group (exactly-once semantics) async function consumeEvents( stream: string, group: string, consumer: string ) { // Create consumer group if not exists try { await redis.xgroup('CREATE', stream, group, '0', 'MKSTREAM'); } catch (e) { // Group already exists, ignore }
while (true) { // Read new messages for this consumer const messages = await redis.xreadgroup( 'GROUP', group, consumer, 'COUNT', 10, 'BLOCK', 5000, 'STREAMS', stream, '>' );
if (!messages) continue;
for (const [, entries] of messages) { for (const [id, fields] of entries) { try { const event = JSON.parse(fields[1]); await processEvent(event);
// Acknowledge successful processing await redis.xack(stream, group, id); } catch (error) { console.error(Failed to process ${id}:, error); // Message will be redelivered to another consumer } } } } }
---
Name
Leaderboard with Sorted Sets
Description
Real-time rankings with O(log n) updates
When
Building scoreboards, rankings, or priority queues
Example
class Leaderboard { constructor(private key: string) {}
async updateScore(userId: string, score: number) { await redis.zadd(this.key, score, userId); }
async incrementScore(userId: string, amount: number) { return redis.zincrby(this.key, amount, userId); }
async getTopN(n: number): Promise<{ userId: string; score: number }[]> { // Get top N with scores, highest first const results = await redis.zrevrange(this.key, 0, n - 1, 'WITHSCORES');
const entries: { userId: string; score: number }[] = []; for (let i = 0; i < results.length; i += 2) { entries.push({ userId: results[i], score: parseFloat(results[i + 1]) }); } return entries; }
async getRank(userId: string): Promise<number | null> { // 0-indexed, highest score = rank 0 const rank = await redis.zrevrank(this.key, userId); return rank !== null ? rank + 1 : null; // Convert to 1-indexed }
async getAroundUser(userId: string, range: number = 5) { const rank = await redis.zrevrank(this.key, userId); if (rank === null) return null;
const start = Math.max(0, rank - range); const end = rank + range;
return redis.zrevrange(this.key, start, end, 'WITHSCORES'); } }
Anti-Patterns
---
Name
Caching Without Invalidation Strategy
Description
Adding cache without planning how to invalidate it
Why
Cache with only TTL leads to stale data. Users see outdated information for the entire TTL duration. Writes appear to be lost. Eventually someone sets TTL to 1 second and you have no cache at all.
Instead
Plan invalidation from the start - cache-aside with explicit delete on update
---
Name
Hot Key Problem
Description
Single key receiving disproportionate traffic
Why
One key getting 100k reads/second hits a single Redis node. That node becomes the bottleneck. Cluster mode does not help because data is on one shard.
Instead
Use read replicas, local caching for hot keys, or shard the key with random suffix
---
Name
Storing Large Values
Description
Caching multi-megabyte objects in Redis
Why
Large values block the single-threaded Redis. A 10MB GET blocks all other operations. Network transfer is slow. Memory usage explodes.
Instead
Store references/IDs, use compression, or use object storage for large blobs
---
Name
Missing TTL on All Keys
Description
Creating keys without expiration
Why
Memory fills up over time. Redis starts evicting random keys (or crashes with OOM). You have no idea what data is still valid. Debugging is impossible.
Instead
Always set TTL. Use maxmemory-policy as safety net, not primary strategy
---
Name
Synchronous Pub/Sub for Critical Data
Description
Using pub/sub for data that must not be lost
Why
Pub/sub is fire-and-forget. If no subscribers are connected, messages are lost. If subscriber disconnects mid-message, it is lost. No replay, no persistence.
Instead
Use Redis Streams with consumer groups for reliable messaging
---
Name
Storing Relational Data
Description
Trying to replicate database relationships in Redis
Why
Redis has no JOINs, no transactions across keys (mostly), no foreign keys. You end up with denormalized data, consistency bugs, and N+1 query patterns in your code.
Instead
Use Redis for caching and specific patterns. Keep relational data in the database
Redis Specialist - Sharp Edges
Cache Stampede on Expiration
Id
cache-stampede
Severity
critical
Situation
High-traffic key expires, hundreds of requests simultaneously query database and try to repopulate cache. Database gets overwhelmed.
Why
TTL-based expiration is deterministic. All requests see cache miss at the same moment. Each request independently decides to fetch from database. N concurrent requests = N identical database queries.
Solution
Use probabilistic early expiration or distributed lock:
// Probabilistic early refresh (XFetch algorithm) async function getWithEarlyRefresh<T>( key: string, ttl: number, fetch: () => Promise<T> ): Promise<T> { const cached = await redis.get(key); if (!cached) { return refreshCache(key, ttl, fetch); }
const { value, expiry, delta } = JSON.parse(cached); const now = Date.now();
// Probabilistic early refresh // As expiry approaches, probability of refresh increases const beta = 1; // Tuning parameter const random = Math.random(); const xfetch = delta beta Math.log(random);
if (now - xfetch >= expiry) { // Refresh in background, return stale value refreshCache(key, ttl, fetch); // No await }
return value; }
// Alternative: Lock-based refresh async function getWithLock<T>(key: string, fetch: () => Promise<T>): Promise<T> { const cached = await redis.get(key); if (cached) return JSON.parse(cached);
const lockKey = lock:${key}; const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 5);
if (!acquired) { // Another process is refreshing, wait and retry await sleep(100); return getWithLock(key, fetch); }
try { const value = await fetch(); await redis.setex(key, 3600, JSON.stringify(value)); return value; } finally { await redis.del(lockKey); } }
Symptoms
- Database CPU spikes at regular intervals matching TTL
- Latency spikes every N seconds/minutes
- Cache hit rate drops to 0% then recovers
- Multiple identical queries in database logs at same timestamp
Detection Pattern
setex.\d{3,}.(?!.*lock|mutex|refresh)
Hot Key Single-Shard Bottleneck
Id
hot-key-bottleneck
Severity
critical
Situation
One key receives disproportionate traffic (celebrity post, viral content, global config). In cluster mode, this key lives on one shard.
Why
Redis Cluster shards by key hash. All requests for one key go to one node. That node becomes bottleneck while others sit idle. Cluster mode provides no help for single-key hotspots.
Solution
Shard hot keys manually with random suffix:
class HotKeyHandler { private shardCount = 10;
async getHotKey(baseKey: string): Promise<string | null> { // Random read from any shard const shard = Math.floor(Math.random() * this.shardCount); const shardKey = ${baseKey}:${shard}; return redis.get(shardKey); }
async setHotKey(baseKey: string, value: string, ttl: number): Promise<void> { // Write to ALL shards const pipeline = redis.multi(); for (let i = 0; i < this.shardCount; i++) { pipeline.setex(${baseKey}:${i}, ttl, value); } await pipeline.exec(); }
async deleteHotKey(baseKey: string): Promise<void> { const pipeline = redis.multi(); for (let i = 0; i < this.shardCount; i++) { pipeline.del(${baseKey}:${i}); } await pipeline.exec(); } }
// Alternative: Local cache for hot keys const localCache = new LRU({ maxAge: 1000 }); // 1 second
async function getWithLocalCache(key: string): Promise<string | null> { const local = localCache.get(key); if (local) return local;
const value = await redis.get(key); if (value) localCache.set(key, value); return value; }
Symptoms
- One Redis node at 100% CPU, others idle
- Cluster rebalancing doesn't help
- Single key dominates slowlog
- Latency spikes for specific keys only
Detection Pattern
get\(['"]config['"]\)|get\(['"]global|get\(['"]featured
Large Values Block Single-Threaded Redis
Id
large-value-blocking
Severity
critical
Situation
Caching serialized objects (user profiles with history, product catalogs, session data with cart). Values grow to megabytes.
Why
Redis is single-threaded. A 10MB GET blocks ALL other operations during network transfer. O(n) commands on large structures (HGETALL, SMEMBERS) block even longer. One bad key affects all traffic.
Solution
Split large data, compress, or use references:
// Split by access pattern // BAD: One big object await redis.set(user:${id}, JSON.stringify({ profile: {...}, // Accessed often preferences: {...}, // Accessed often orderHistory: [...], // Accessed rarely, can be huge activityLog: [...] // Accessed rarely, grows forever }));
// GOOD: Split by access pattern await redis.hmset(user:${id}:core, { profile: JSON.stringify(profile), preferences: JSON.stringify(preferences) }); await redis.set(user:${id}:orders, JSON.stringify(orders)); // Don't cache activity log - query from database
// Compress large values import { gzip, gunzip } from 'zlib'; import { promisify } from 'util';
const compress = promisify(gzip); const decompress = promisify(gunzip);
async function setCompressed(key: string, value: any, ttl: number) { const json = JSON.stringify(value); if (json.length > 1024) { // Only compress if worth it const compressed = await compress(Buffer.from(json)); await redis.setex(${key}:gz, ttl, compressed.toString('base64')); } else { await redis.setex(key, ttl, json); } }
Symptoms
- Slowlog shows simple GET commands taking >100ms
- Memory usage grows faster than expected
- Latency increases as data grows
- DEBUG OBJECT shows large serialized lengths
Detection Pattern
JSON\.stringify.\{[^}]{500,}|setex.JSON\.stringify\(.\[.\]
Keys Without TTL Fill Memory
Id
missing-ttl-memory-exhaustion
Severity
high
Situation
Creating keys without expiration. Session keys, cache keys, temporary data. Memory grows over time until Redis evicts or crashes.
Why
Redis keeps keys forever unless explicitly deleted or expired. With maxmemory-policy, Redis evicts but you lose control over WHAT gets evicted. volatile-lru only evicts keys WITH TTL - permanent keys never evicted.
Solution
Always set TTL, even if long:
// Wrapper that enforces TTL class SafeRedis { private defaultTTL = 86400; // 24 hours private maxTTL = 604800; // 7 days
async set(key: string, value: string, ttl?: number): Promise<void> { const effectiveTTL = Math.min(ttl || this.defaultTTL, this.maxTTL); await redis.setex(key, effectiveTTL, value); }
// Audit existing keys async auditNoTTL(): Promise<string[]> { const noTTL: string[] = []; let cursor = '0';
do { const [newCursor, keys] = await redis.scan(cursor, 'COUNT', 100); cursor = newCursor;
for (const key of keys) { const ttl = await redis.ttl(key); if (ttl === -1) { // No expiry noTTL.push(key); } } } while (cursor !== '0');
return noTTL; } }
// Also configure Redis safety net // redis.conf: // maxmemory 2gb // maxmemory-policy volatile-lru # Evict keys with TTL first
Symptoms
- Memory usage grows linearly over time
- redis-cli INFO memory shows used_memory increasing
- DBSIZE grows without corresponding deletes
- Eventually OOM or mass eviction
Detection Pattern
redis\.set\([^)]+\)(?!.*ex|EX|setex|SETEX|expire)
Pub/Sub Messages Lost During Disconnect
Id
pubsub-message-loss
Severity
high
Situation
Using Redis pub/sub for critical data (orders, notifications, state sync). Subscriber disconnects briefly, misses messages.
Why
Pub/sub is fire-and-forget. No persistence, no acknowledgment, no replay. If subscriber is disconnected when message published, message is gone forever. Even connected subscribers may lose messages under memory pressure.
Solution
Use Redis Streams for reliable messaging:
// Producer with Streams (reliable) async function publishReliable(stream: string, data: object): Promise<string> { const id = await redis.xadd( stream, 'MAXLEN', '~', 10000, // Approximate trim to 10k entries '*', // Auto-generate ID 'data', JSON.stringify(data) ); return id; // Returns message ID for tracking }
// Consumer Group (exactly-once semantics) class ReliableConsumer { async consume(stream: string, group: string, consumer: string) { // Create group if not exists try { await redis.xgroup('CREATE', stream, group, '0', 'MKSTREAM'); } catch (e) { // Group exists, OK }
while (true) { // First: Claim pending messages (from crashed consumers) const pending = await redis.xautoclaim( stream, group, consumer, 60000, // Claim if idle > 60s '0-0', 'COUNT', 10 );
for (const [id, fields] of pending[1]) { await this.process(fields); await redis.xack(stream, group, id); }
// Then: Read new messages const messages = await redis.xreadgroup( 'GROUP', group, consumer, 'BLOCK', 5000, 'COUNT', 10, 'STREAMS', stream, '>' );
if (!messages) continue;
for (const [, entries] of messages) { for (const [id, fields] of entries) { await this.process(fields); await redis.xack(stream, group, id); } } } } }
Symptoms
- Missing events in event-driven systems
- Inconsistent state between services
- "Works most of the time" - fails under load or restart
- No way to replay or debug what was sent
Detection Pattern
redis\.publish|subscribe\(|on\(['"]message
Distributed Lock Without Proper Release
Id
distributed-lock-deadlock
Severity
high
Situation
Acquiring distributed lock but process crashes before releasing. Or lock TTL too short, operation takes longer, lock expires mid-operation.
Why
Lock TTL must be longer than maximum operation time. If operation takes longer, another process acquires lock while first still running. Two processes in critical section = data corruption.
Solution
Use Redlock with proper TTL and extend mechanism:
import Redlock from 'redlock';
const redlock = new Redlock([redis], { retryCount: 3, retryDelay: 200, retryJitter: 100 });
async function withLock<T>( resource: string, ttl: number, fn: (extend: (ms: number) => Promise<void>) => Promise<T> ): Promise<T> { const lock = await redlock.acquire([lock:${resource}], ttl);
try { // Pass extend function to operation const extend = async (ms: number) => { await lock.extend(ms); };
return await fn(extend); } finally { await lock.release(); } }
// Usage with extension for long operations await withLock('order:123', 10000, async (extend) => { await step1(); // 3 seconds
// Extend if operation takes longer than expected await extend(10000);
await step2(); // 5 seconds await step3(); // 2 seconds });
// Fencing token pattern for extra safety let fenceToken = 0;
async function acquireWithFence(resource: string): Promise<number> { const token = ++fenceToken; await redis.set(lock:${resource}, token, 'NX', 'EX', 30); return token; }
async function writeWithFence(resource: string, token: number, value: any) { const currentToken = await redis.get(lock:${resource}); if (parseInt(currentToken) !== token) { throw new Error('Lock lost - another process acquired it'); } await db.write(resource, value); }
Symptoms
- Operations occasionally run concurrently when they shouldn't
- Data corruption under load
- Lock acquired but operation still failed
- Deadlocks when process crashes holding lock
Detection Pattern
setnx.lock(?!.finally.*del|release)
KEYS Command Blocks Production
Id
scan-blocking-production
Severity
high
Situation
Using KEYS command to find keys by pattern. Works in development, blocks production Redis for seconds.
Why
KEYS is O(n) and blocks Redis during entire scan. With millions of keys, this can take seconds. All other operations queue behind it. One bad KEYS command can take down your entire application.
Solution
Use SCAN for production key iteration:
// BAD: Blocks Redis const keys = await redis.keys('user:*:session');
// GOOD: Non-blocking iteration async function* scanKeys(pattern: string): AsyncGenerator<string> { let cursor = '0';
do { const [newCursor, keys] = await redis.scan( cursor, 'MATCH', pattern, 'COUNT', 100 // Batch size hint ); cursor = newCursor;
for (const key of keys) { yield key; } } while (cursor !== '0'); }
// Usage for await (const key of scanKeys('user:*:session')) { await processKey(key); }
// Batch operations async function deleteByPattern(pattern: string): Promise<number> { let deleted = 0; const batch: string[] = [];
for await (const key of scanKeys(pattern)) { batch.push(key);
if (batch.length >= 100) { await redis.del(...batch); deleted += batch.length; batch.length = 0; } }
if (batch.length > 0) { await redis.del(...batch); deleted += batch.length; }
return deleted; }
// Disable KEYS in production // redis.conf: rename-command KEYS ""
Symptoms
- Periodic latency spikes in Redis
- Redis slowlog shows KEYS command
- All operations timeout during key enumeration
- Works in dev, dies in production
Detection Pattern
redis\.keys\(|KEYS ['"]\*
MULTI/EXEC Is Not ACID Transaction
Id
transaction-wrong-expectations
Severity
medium
Situation
Using MULTI/EXEC expecting database-style transactions with rollback. Conditional logic inside transaction fails silently.
Why
Redis MULTI/EXEC is atomic execution, not ACID. Commands are queued, not executed until EXEC. You can't read values and branch during transaction. No rollback - if one command fails, others still execute.
Solution
Use Lua scripts for atomic read-modify-write:
// BAD: This doesn't work as expected const multi = redis.multi(); const balance = await multi.get('balance'); // Returns QUEUED, not value! if (balance > 100) { multi.decrby('balance', 100); } await multi.exec();
// GOOD: Lua script for atomic operations const transferScript = ` local balance = tonumber(redis.call('GET', KEYS[1])) or 0 local amount = tonumber(ARGV[1])
if balance >= amount then redis.call('DECRBY', KEYS[1], amount) redis.call('INCRBY', KEYS[2], amount) return 1 else return 0 end `;
async function transfer(from: string, to: string, amount: number): Promise<boolean> { const result = await redis.eval( transferScript, 2, // Number of KEYS balance:${from}, // KEYS[1] balance:${to}, // KEYS[2] amount.toString() // ARGV[1] ); return result === 1; }
// WATCH for optimistic locking async function incrementIfExists(key: string): Promise<boolean> { await redis.watch(key);
const exists = await redis.exists(key); if (!exists) { await redis.unwatch(); return false; }
const result = await redis.multi() .incr(key) .exec();
return result !== null; // null means WATCH failed }
Symptoms
- Race conditions in "transactional" code
- Conditional logic inside MULTI doesn't work
- Partial updates when expecting all-or-nothing
- Values read inside MULTI are undefined
Detection Pattern
multi\(\).get\(|if.multi.*exec
Connection Pool Exhaustion Under Load
Id
connection-pool-exhaustion
Severity
medium
Situation
Application creates new Redis connections per request or doesn't properly pool connections. Under load, runs out of connections or file descriptors.
Why
Redis connections are TCP sockets - expensive to create. Without pooling, each request creates new connection, uses it once, closes it. Under load, you hit connection limits or file descriptor limits.
Solution
Use proper connection pooling:
import Redis from 'ioredis';
// Single connection for most use cases const redis = new Redis({ host: 'localhost', port: 6379, maxRetriesPerRequest: 3, retryStrategy: (times) => Math.min(times * 50, 2000), enableReadyCheck: true, lazyConnect: false });
// Cluster with built-in pooling const cluster = new Redis.Cluster([ { host: 'node1', port: 6379 }, { host: 'node2', port: 6379 }, { host: 'node3', port: 6379 } ], { scaleReads: 'slave', clusterRetryStrategy: (times) => Math.min(times * 100, 3000) });
// For pub/sub, use separate connection const subscriber = redis.duplicate();
// Monitor connection health redis.on('error', (err) => console.error('Redis error:', err)); redis.on('connect', () => console.log('Redis connected')); redis.on('ready', () => console.log('Redis ready')); redis.on('close', () => console.log('Redis closed'));
// Graceful shutdown process.on('SIGTERM', async () => { await redis.quit(); // Waits for pending commands process.exit(0); });
Symptoms
- "ECONNREFUSED" or "too many connections" errors
- Connection count in Redis INFO climbs continuously
- File descriptor exhaustion on application server
- Slow performance during connection storm
Detection Pattern
new Redis\(.\)(?=.new Redis)|createClient\(\)(?=.*createClient)
Data Type Loss in JSON Serialization
Id
serialization-type-loss
Severity
medium
Situation
Storing JavaScript objects in Redis via JSON.stringify. Dates become strings, Sets become objects, undefined values disappear.
Why
JSON has limited type support. Date becomes ISO string, Set/Map become {}, undefined values are stripped, BigInt throws error. Round-trip through cache changes your data.
Solution
Use type-preserving serialization:
// Custom replacer/reviver for Date function serialize(value: any): string { return JSON.stringify(value, (key, val) => { if (val instanceof Date) { return { __type: 'Date', value: val.toISOString() }; } if (val instanceof Set) { return { __type: 'Set', value: [...val] }; } if (val instanceof Map) { return { __type: 'Map', value: [...val] }; } return val; }); }
function deserialize<T>(json: string): T { return JSON.parse(json, (key, val) => { if (val && typeof val === 'object' && val.__type) { switch (val.__type) { case 'Date': return new Date(val.value); case 'Set': return new Set(val.value); case 'Map': return new Map(val.value); } } return val; }); }
// Or use superjson for automatic handling import superjson from 'superjson';
await redis.set(key, superjson.stringify(value)); const restored = superjson.parse(await redis.get(key));
// Use Redis native types when possible // Date -> store as timestamp (ZADD for sorted access) // Set -> use Redis SET (SADD, SMEMBERS) // Hash -> use Redis HASH (HSET, HGETALL)
Symptoms
- Dates compared as strings fail
- Object equality checks fail after cache round-trip
- Missing properties after deserialization
- Sets/Maps become empty objects
Detection Pattern
JSON\.parse.Date|new Date\(.JSON
Wrong Eviction Policy for Workload
Id
eviction-policy-mismatch
Severity
medium
Situation
Using default eviction policy (noeviction or volatile-lru) for workload that doesn't match. Cache fills up, either crashes or evicts wrong keys.
Why
Different workloads need different eviction. LRU evicts least recently used, but some old keys are more valuable than recent ones. TTL-only eviction requires ALL keys to have TTL.
Solution
Match eviction policy to workload:
For cache (can regenerate all data)
maxmemory-policy allkeys-lru
For cache with TTL (only evict expiring keys)
maxmemory-policy volatile-lru
For sessions (approximate LRU is fine)
maxmemory-policy allkeys-lru
For leaderboards (need to keep all scores)
maxmemory-policy noeviction # And monitor closely!
For mix of cache + permanent data
Use separate Redis instances or keyspace prefixes
// Monitor eviction class EvictionMonitor { async check(): Promise<void> { const info = await redis.info('stats'); const evicted = this.parseEvictedKeys(info);
if (evicted > 0) { console.warn(Redis evicted ${evicted} keys); // Alert if eviction rate is high } }
async memoryUsage(): Promise<{ used: number; max: number; pct: number }> { const [used, max] = await Promise.all([ redis.info('memory').then(i => this.parseUsedMemory(i)), redis.config('GET', 'maxmemory').then(c => parseInt(c[1])) ]);
return { used, max, pct: (used / max) * 100 }; } }
Symptoms
- OOM errors with noeviction policy
- Important keys disappearing unexpectedly
- Cache hit rate drops as memory fills
- Inconsistent behavior between dev (small data) and prod (full data)
Detection Pattern
maxmemory-policy|CONFIG SET maxmemory
Redis Specialist - Validations
Redis SET Without TTL
Id
missing-ttl
Severity
error
Type
regex
Pattern
- redis\.set\([^)]+\)(?!.*[Ee][Xx]|setex)
- \.set\(['"][^'"]+['"],\s[^,]+\)(?!.[Ee][Xx])
Message
Redis SET without TTL. Keys without expiration fill memory until eviction or OOM.
Fix Action
Use setex() or set() with EX option to ensure keys expire
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
KEYS Command in Production Code
Id
keys-command
Severity
error
Type
regex
Pattern
- redis\.keys\(
- \.keys\(['"]\*
- KEYS ['"]\*
Message
KEYS command blocks Redis during scan. O(n) on entire keyspace.
Fix Action
Use SCAN with cursor for non-blocking iteration
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Pub/Sub for Critical Messages
Id
pubsub-for-critical-data
Severity
error
Type
regex
Pattern
- publish.*order|payment|transaction
- subscribe.*order|payment|transaction
Message
Using pub/sub for critical data. Messages lost if subscriber disconnects.
Fix Action
Use Redis Streams with consumer groups for reliable messaging
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Cache Write Without Invalidation Plan
Id
no-cache-invalidation
Severity
warning
Type
regex
Pattern
- setex.user|profile|settings(?!.del|invalidate)
- cache.set(?!.on.*update|invalidat)
Message
Caching data without clear invalidation strategy leads to stale data.
Fix Action
Implement explicit cache invalidation on data updates
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Distributed Lock Without Finally Block
Id
lock-without-finally
Severity
error
Type
regex
Pattern
- acquire.lock(?!.finally.*release)
- setnx.lock(?!.finally.*del)
- redlock\.acquire(?!.*finally)
Message
Lock acquired without guaranteed release in finally block.
Fix Action
Always release locks in finally block to prevent deadlocks
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Large Object Serialization
Id
large-json-stringify
Severity
warning
Type
regex
Pattern
- JSON\.stringify.\[.\.map|filter|reduce
- setex.JSON\.stringify.array|list|history
Message
Storing large serialized arrays. Consider splitting or using Redis lists.
Fix Action
Use Redis native data structures (LPUSH, ZADD) or split large objects
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
New Redis Connection Per Request
Id
connection-per-request
Severity
error
Type
regex
Pattern
- new Redis\(.\)(?=.req|request|handler)
- createClient\(\)(?=.async.req)
Message
Creating new Redis connection per request. Use connection pool.
Fix Action
Create single Redis client at module level and reuse
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
MULTI Without EXEC
Id
multi-without-exec
Severity
error
Type
regex
Pattern
- \.multi\(\)(?!.*\.exec\()
Message
MULTI transaction started but EXEC not called. Commands never execute.
Fix Action
Always call exec() to execute transaction
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Naive Rate Limiting Implementation
Id
rate-limit-naive
Severity
warning
Type
regex
Pattern
- incr.expire(?!.multi|pipeline)
- incr.ttl(?!.multi|atomic)
Message
Non-atomic rate limiting. INCR and EXPIRE should be in transaction.
Fix Action
Use MULTI/EXEC or Lua script for atomic rate limiting
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
HGETALL on Potentially Large Hash
Id
hgetall-large-hash
Severity
warning
Type
regex
Pattern
- hgetall.*user|session|product|order
- HGETALL ['"][^'"]+['"]
Message
HGETALL returns entire hash. For large hashes, use HSCAN or HMGET.
Fix Action
Use HMGET for specific fields or HSCAN for iteration
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
SMEMBERS on Potentially Large Set
Id
smembers-large-set
Severity
warning
Type
regex
Pattern
- smembers.*all|users|members|followers
- SMEMBERS ['"][^'"]+['"]
Message
SMEMBERS returns entire set. For large sets, use SSCAN.
Fix Action
Use SSCAN for iteration or SISMEMBER for membership check
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Redis Client Without Error Handler
Id
no-error-handler
Severity
warning
Type
regex
Pattern
- new Redis\((?!.on.error)
- createClient\((?!.on.error)
Message
Redis client without error handler. Unhandled errors crash Node process.
Fix Action
Add redis.on('error', handler) to prevent process crash
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Blocking Operation Without Timeout
Id
blocking-operation-no-timeout
Severity
warning
Type
regex
Pattern
- blpop|brpop|brpoplpush(?!.*\d{3,})
- BLPOP|BRPOP(?!.*TIMEOUT)
Message
Blocking operation without timeout can hang indefinitely.
Fix Action
Always specify timeout for blocking operations (0 means forever)
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Lua Script Defined Inline
Id
lua-script-inline
Severity
info
Type
regex
Pattern
- eval\('"
- \.eval\(`.*redis\.call
Message
Lua script defined inline. Consider loading scripts with SCRIPT LOAD for reuse.
Fix Action
Use SCRIPT LOAD and EVALSHA for frequently used scripts
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx