Skip to main content

Redis Data Structure Chooser

Pick the right Redis data type for every use case. Covers Strings, Hashes, Lists, Sets, Sorted Sets, and Streams with command examples and real-world patterns.

✓ All 6 data types ✓ Decision guide ✓ Rate limiter pattern ✓ Leaderboard pattern

Quick Decision Guide

Use Case Data Type Key Command
Cached value / counterStringSET, GET, INCR, INCRBY
Object with partial updatesHashHSET, HGET, HMGET, HDEL
Queue / stack / timelineListLPUSH, RPOP, LRANGE
Unique tags / permissionsSetSADD, SMEMBERS, SISMEMBER
Leaderboard / rate limitSorted SetZADD, ZREVRANK, ZRANGEBYSCORE
Event log / task queueStreamXADD, XREAD, XGROUP, XACK
Broadcast to live subscribersPub/SubPUBLISH, SUBSCRIBE

String — Caching, Counters, Sessions

// Cache with TTL (TypeScript with ioredis)
await redis.set('cache:product:123', JSON.stringify(product), 'EX', 3600);
const cached = await redis.get('cache:product:123');

// Atomic counter
await redis.incr('counter:page_views');
await redis.incrby('counter:score:user:5', 10);

// SET NX (set only if not exists) — distributed lock
const acquired = await redis.set('lock:resource', '1', 'EX', 30, 'NX');
if (!acquired) throw new Error('Lock not acquired');

Hash — Object Storage with Field Access

// Store user profile — update individual fields without full rewrite
await redis.hset('user:123', {
  name: 'Alice',
  email: '[email protected]',
  plan: 'pro',
  last_login: Date.now(),
});

// Read specific fields only
const [name, plan] = await redis.hmget('user:123', 'name', 'plan');

// Increment a numeric hash field
await redis.hincrby('user:123', 'login_count', 1);

// Get all fields
const user = await redis.hgetall('user:123');

Sorted Set — Leaderboard

// Add/update score
await redis.zadd('leaderboard:game1', score, userId);

// Top 10 players (highest score first)
const top10 = await redis.zrevrange('leaderboard:game1', 0, 9, 'WITHSCORES');

// Player's rank (0-indexed, add 1 for display)
const rank = await redis.zrevrank('leaderboard:game1', userId);

// Player's score
const score = await redis.zscore('leaderboard:game1', userId);

Sorted Set — Sliding Window Rate Limiter

async function isRateLimited(userId: string, limitPerMinute: number): Promise<boolean> {
  const key = `rate_limit:${userId}`;
  const now = Date.now();
  const windowMs = 60 * 1000;

  const pipeline = redis.pipeline();
  // Remove entries older than 1 minute
  pipeline.zremrangebyscore(key, 0, now - windowMs);
  // Add current request with timestamp as score
  pipeline.zadd(key, now, `${now}-${Math.random()}`);
  // Count requests in the window
  pipeline.zcard(key);
  // Expire key after window (cleanup)
  pipeline.expire(key, 70);

  const results = await pipeline.exec();
  const count = results![2][1] as number;
  return count > limitPerMinute;
}

Streams — Reliable Event Queue

// Producer: add events to stream
await redis.xadd('events:orders', '*', {
  orderId: '12345',
  userId: '99',
  amount: '49.99',
  event: 'created',
});

// Consumer group setup (once)
await redis.xgroup('CREATE', 'events:orders', 'processors', '$', 'MKSTREAM');

// Consumer: read unprocessed messages
const messages = await redis.xreadgroup(
  'GROUP', 'processors', 'worker-1',
  'COUNT', 10, 'BLOCK', 2000,
  'STREAMS', 'events:orders', '>'
);

// Acknowledge after processing
if (messages) {
  for (const [stream, entries] of messages) {
    for (const [id, fields] of entries) {
      await processOrder(fields);
      await redis.xack('events:orders', 'processors', id);
    }
  }
}

Frequently Asked Questions

When should I use a Redis String vs Hash for storing user sessions?▼

Use String for simple key-value storage where the entire value is fetched/written atomically — like a serialized session JSON. Use Hash when you need to read or update individual fields without fetching the entire object: HGET session:123 userId is more efficient than GET+deserialize+re-serialize when you only need one field. For sessions with 3-5 fields, Hash is typically more memory-efficient and enables partial updates. For complex nested structures, JSON with RedisJSON module or a serialized String is simpler.

What is a Redis Sorted Set (ZSet) and what are its typical use cases?▼

A Sorted Set stores unique members, each with an associated floating-point score, and maintains members sorted by score at all times. Operations are O(log N). Common use cases: leaderboards (score = points, rank by ZREVRANK), rate limiting (score = timestamp, ZRANGEBYSCORE to count events in a time window), job priority queues (score = priority), activity feeds (score = Unix timestamp for chronological ordering), and autocomplete (store prefix strings with a score and ZRANGEBYLEX for prefix queries).

How do I implement a Redis-based rate limiter?▼

Use a Sorted Set per user. Key = rate_limit:user:{id}. On each request: ZADD key currentTimestamp uniqueRequestId (NX), ZREMRANGEBYSCORE key 0 (currentTime - windowMs) to evict old entries, ZCARD key to count requests in the window, expire the key with EXPIRE. If count > limit, reject the request. This sliding window approach is more accurate than a fixed window counter. For very high throughput, the fixed window approach (INCR + EXPIRE) is simpler and faster at the cost of accuracy at window boundaries.

When should I use Redis Pub/Sub vs Streams?▼

Pub/Sub is for fire-and-forget messaging where offline subscribers miss messages — use for live chat, real-time notifications, or broadcasting to connected clients. Redis Streams (XADD/XREAD) are like a persistent log with consumer groups — subscribers that disconnect can resume from where they left off using the last message ID. Use Streams for event sourcing, reliable task queues, audit logs, or any case where you cannot afford to miss messages. Streams support multiple consumer groups and acknowledgment (XACK), making them suitable for producer-consumer patterns.

What is the difference between EXPIRE and EXPIREAT in Redis?▼

EXPIRE sets a timeout in seconds from the current time: EXPIRE key 3600 expires in 1 hour. EXPIREAT sets an absolute Unix timestamp for expiration: EXPIREAT key 1735689600. PEXPIRE and PEXPIREAT work the same way but in milliseconds. Use EXPIREAT when you need to expire a key at a specific wall-clock time (e.g., end of business day). Use EXPIRE for relative TTLs. PERSIST removes an existing TTL. TTL key returns remaining seconds (-1 = no TTL, -2 = key doesn't exist).

How do I handle Redis key naming conventions for large applications?▼

Use colon-separated namespaces: type:entity:id:field — for example session:user:12345, cache:product:slug:iphone-15, rate_limit:api:user:99. Always document your key schema. Avoid key patterns that make KEYS * dangerous (it blocks Redis) — use SCAN with cursor instead for key enumeration. Set appropriate TTLs on all keys to prevent unbounded memory growth. Group related keys with consistent prefixes to enable targeted flushing with SCAN + DEL patterns. Use short but descriptive names — Redis key size affects memory; avoid excessively long keys.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →