All topics
Database · Learning hub

Redis notes for developers

Master Redis with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Database notes
Redis

Data Structures & Commands

Data Structures & Commands Strings # Basic get/set SET name "Alice" GET name # "Alice" DEL name EXISTS name # 0 or 1 # With expiry SET session:abc "data" EX 360

Data Structures & Commands

Strings

# Basic get/set
SET name "Alice"
GET name                  # "Alice"
DEL name
EXISTS name               # 0 or 1

# With expiry
SET session:abc "data" EX 3600      # expire in 3600 seconds
SET session:abc "data" PX 60000     # expire in 60000 milliseconds
SET session:abc "data" EXAT 1700000000   # expire at Unix timestamp
TTL session:abc           # seconds remaining (-1 = no expiry, -2 = gone)
PERSIST session:abc       # remove TTL

# NX / XX options
SET user:1 "Alice" NX     # set only if NOT exists (atomic "insert")
SET user:1 "Bob" XX       # set only if EXISTS (atomic "update")

# Numeric operations (atomic!)
SET counter 0
INCR counter              # 1
INCRBY counter 10         # 11
DECR counter              # 10
DECRBY counter 3          # 7
INCRBYFLOAT price 1.5

# Bulk operations
MSET k1 v1 k2 v2 k3 v3
MGET k1 k2 k3             # [v1, v2, v3]

# String operations
APPEND greeting "Hello"
APPEND greeting " World"
STRLEN greeting           # 11
GETSET key newvalue       # returns old, sets new
GETDEL key                # returns value, deletes key

Lists

# LPUSH/RPUSH — add to head/tail
LPUSH tasks "task1" "task2"    # task2 is now head
RPUSH tasks "task3"
LRANGE tasks 0 -1             # get all elements (0 to last)
LLEN tasks                    # length

# Pop from head or tail
LPOP tasks                    # remove + return head
RPOP tasks                    # remove + return tail
LPOP tasks 2                  # pop 2 elements

# Blocking pop (for queue workers — waits up to N seconds)
BLPOP queue:jobs 30           # blocks until item available or timeout

# Index access
LINDEX tasks 0                # get head without removing
LSET tasks 0 "updated-task"

# Trim
LTRIM logs 0 999              # keep only first 1000 items

# Use cases
# Stack: LPUSH + LPOP
# Queue: RPUSH + BLPOP
# Capped log: RPUSH + LTRIM 0 999

Sets & Sorted Sets

# Sets — unique, unordered
SADD tags "js" "ts" "react"
SREM tags "react"
SMEMBERS tags               # all members
SISMEMBER tags "js"         # 1 (true) or 0 (false)
SCARD tags                  # count
SMISMEMBER tags "js" "go"   # [1, 0]

# Set operations
SUNION set1 set2            # union
SINTER set1 set2            # intersection
SDIFF set1 set2             # difference (in set1, not in set2)
SUNIONSTORE dest set1 set2  # store result

# Sorted Sets — unique members with a score
ZADD leaderboard 1000 "alice" 850 "bob" 950 "carol"
ZINCRBY leaderboard 50 "bob"                    # bob score += 50
ZSCORE leaderboard "alice"                      # 1000
ZRANK leaderboard "alice"                       # 0-based rank (lowest score first)
ZREVRANK leaderboard "alice"                    # rank from highest score
ZRANGE leaderboard 0 -1                         # sorted by score asc
ZRANGE leaderboard 0 -1 REV                     # desc
ZRANGE leaderboard 0 -1 WITHSCORES             # include scores
ZRANGEBYSCORE leaderboard 900 1100              # by score range
ZRANGEBYLEX leaderboard "[a" "[m"               # by lex range (when scores equal)
ZCARD leaderboard                               # count
ZREM leaderboard "bob"
ZPOPMIN leaderboard         # remove lowest score member
ZPOPMAX leaderboard         # remove highest score member

# Use cases: leaderboards, rate limiting, time-sorted events

Hashes

# Hashes — object-like, field:value pairs
HSET user:1 name "Alice" email "alice@example.com" age 28
HGET user:1 name            # "Alice"
HMGET user:1 name email     # ["Alice", "alice@example.com"]
HGETALL user:1              # all field-value pairs
HKEYS user:1                # all keys
HVALS user:1                # all values
HLEN user:1                 # field count
HDEL user:1 age
HEXISTS user:1 email        # 1
HINCRBY user:1 points 10
HSETNX user:1 name "Bob"    # set only if field doesn't exist

# Use case: cache full objects instead of encoding to string
# Better memory efficiency for many small objects vs separate keys

Key Management

# Key patterns
KEYS user:*               # DANGER: blocks server — never use in prod
SCAN 0 MATCH user:* COUNT 100   # iterative, production-safe
TYPE key                  # string / list / set / zset / hash / stream
OBJECT ENCODING key       # internal encoding (embstr, listpack, skiplist...)
RENAME oldkey newkey
RENAMENX oldkey newkey    # only if newkey doesn't exist
COPY source dest          # copy value to new key
DUMP key / RESTORE key TTL serialized   # serialize/deserialize

# Expiry
EXPIRE key 60             # set TTL in seconds
EXPIREAT key 1700000000  # expire at Unix timestamp
PEXPIRE key 60000         # milliseconds
TTL key                   # remaining seconds
PTTL key                  # remaining ms
Redis

Caching, Pub/Sub & Patterns

Caching, Pub/Sub & Patterns Caching Patterns // Cache-aside (lazy loading) — most common async function getUser(id) { const cacheKey = `user:${id}`; const cache

Caching, Pub/Sub & Patterns

Caching Patterns

// Cache-aside (lazy loading) — most common
async function getUser(id) {
  const cacheKey = `user:${id}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.findUser(id);
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 300);  // 5 min TTL
  return user;
}

// Write-through — update cache on every write
async function updateUser(id, data) {
  const user = await db.updateUser(id, data);
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300);
  return user;
}

// Cache invalidation on write
async function deleteUser(id) {
  await db.deleteUser(id);
  await redis.del(`user:${id}`);
}

// Hash-based caching for partial updates
async function updateUserField(id, field, value) {
  await db.updateUser(id, { [field]: value });
  await redis.hset(`user:${id}`, field, value);
}

// Multi-key invalidation pattern (tag-based)
await redis.sadd(`tag:user:${userId}:keys`, `posts:${userId}`, `activity:${userId}`);
// On user update, get all related keys and delete them
const keys = await redis.smembers(`tag:user:${userId}:keys`);
if (keys.length) await redis.del(...keys);

Rate Limiting

// Fixed window rate limiter
async function checkRateLimit(ip, limit = 100, windowSec = 60) {
  const key = `rate:${ip}:${Math.floor(Date.now() / (windowSec * 1000))}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, windowSec);
  return count <= limit;
}

// Sliding window with sorted set (more accurate)
async function slidingWindowLimit(ip, limit = 100, windowMs = 60000) {
  const key = `rate:sliding:${ip}`;
  const now = Date.now();
  const windowStart = now - windowMs;
  const requestId = `${now}-${Math.random()}`;

  const pipe = redis.pipeline();
  pipe.zadd(key, now, requestId);
  pipe.zremrangebyscore(key, 0, windowStart);   // remove old requests
  pipe.zcard(key);
  pipe.expire(key, Math.ceil(windowMs / 1000));
  const results = await pipe.exec();

  const count = results[2][1];
  return count <= limit;
}

Pub/Sub

// Publisher (ioredis)
import Redis from 'ioredis';
const pub = new Redis();

await pub.publish('notifications', JSON.stringify({
  type: 'NEW_MESSAGE',
  userId: 42,
  text: 'Hello!',
}));

// Subscriber (separate connection required!)
const sub = new Redis();
sub.subscribe('notifications', 'alerts');

sub.on('message', (channel, message) => {
  const data = JSON.parse(message);
  console.log(`[channel:${channel}]`, data);
});

// Pattern subscribe
sub.psubscribe('user:*:events');
sub.on('pmessage', (pattern, channel, message) => {
  console.log(pattern, channel, message);
});

// Note: sub connection can ONLY subscribe — cannot run other commands
// Use a separate connection for regular commands

Distributed Lock

// Simple distributed lock with SET NX
async function acquireLock(resource, ttlMs = 5000) {
  const lockKey = `lock:${resource}`;
  const lockValue = crypto.randomUUID();  // unique ID for this lock holder
  const result = await redis.set(lockKey, lockValue, 'PX', ttlMs, 'NX');
  return result === 'OK' ? lockValue : null;
}

async function releaseLock(resource, lockValue) {
  // Atomic check-and-delete using Lua to prevent releasing someone else's lock
  const script = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  `;
  return redis.eval(script, 1, `lock:${resource}`, lockValue);
}

// Usage
const lockValue = await acquireLock('send-email', 10000);
if (!lockValue) {
  throw new Error('Could not acquire lock');
}
try {
  await sendEmail();
} finally {
  await releaseLock('send-email', lockValue);
}

Transactions & Pipelining

// Pipeline — batch commands (no atomicity guarantee, but fewer round trips)
const pipe = redis.pipeline();
pipe.set('k1', 'v1');
pipe.get('k1');
pipe.incr('counter');
const results = await pipe.exec();  // [[null, 'OK'], [null, 'v1'], [null, 1]]

// Transaction (MULTI/EXEC — atomic, all or nothing)
const results = await redis
  .multi()
  .set('balance:alice', 900)
  .set('balance:bob', 1100)
  .exec();  // either both succeed or both fail

// WATCH — optimistic locking
async function transfer(from, to, amount) {
  await redis.watch(`balance:${from}`);
  const fromBalance = parseInt(await redis.get(`balance:${from}`));
  if (fromBalance < amount) {
    await redis.unwatch();
    throw new Error('Insufficient funds');
  }
  const result = await redis
    .multi()
    .decrby(`balance:${from}`, amount)
    .incrby(`balance:${to}`, amount)
    .exec();  // returns null if WATCH detected a change
  if (!result) throw new Error('Transaction aborted (concurrent modification)');
}
Redis

Interview Questions

Redis Interview Questions Q: What is Redis and what is it used for? Redis is an in-memory data structure store used as a cache, message broker, session store, r

Redis Interview Questions

Q: What is Redis and what is it used for?

Redis is an in-memory data structure store used as a cache, message broker, session store, rate limiter, and real-time leaderboard. All data is in RAM, making it extremely fast (sub-millisecond reads/writes). It supports optional persistence (RDB snapshots, AOF log) for durability.

Q: What are the main data types in Redis?

  • String — text, numbers, binary data (max 512MB)

  • List — ordered linked list, queue/stack operations

  • Set — unordered unique elements, set operations (union, intersection, diff)

  • Sorted Set (ZSet) — unique elements with a score, sorted by score — leaderboards, time-based data

  • Hash — field:value pairs, like a flat object

  • Stream — append-only log, like Kafka — event sourcing

Q: How does Redis persistence work?

RDB (Redis Database) — periodic snapshots of the dataset to disk. Fast to load on restart, but data written since last snapshot is lost on crash. AOF (Append Only File) — logs every write command. Can be configured to fsync always/every second/never. AOF provides much better durability but larger files. Both can be used together.

Q: What is the difference between KEYS and SCAN?

KEYS pattern blocks the server until it scans the entire keyspace — never use in production. SCAN uses cursor-based iteration, returning a small batch of keys per call without blocking. Iterate until cursor returns 0.

Q: How do you handle cache invalidation?

Three strategies: (1) TTL — set expiry and let cache expire naturally (eventual consistency). (2) Active invalidation — delete/update cache key when the underlying data changes. (3) Event-driven — publish a cache invalidation event and all consumers clear their local caches. Cache invalidation is notoriously hard — one of the "two hard problems" in CS.

Q: Is Redis single-threaded?

The command processing engine is single-threaded (one command at a time), which makes all operations atomic without locking. Since Redis 6.0, I/O threads handle reading/writing network data in parallel, improving throughput. Background threads handle persistence (RDB, AOF rewrite) and lazy freeing.

Keep your Redis knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever