Redis for Node Engineers

One Data Structure Server, a Dozen System-Design Superpowers

Redis for Node Engineers

A Redis deep dive through ioredis — core data structures, pipelines and transactions, pub/sub, why single-threaded is a feature, and the patterns it unlocks: cache, session store, rate limiter, leaderboard.

10 min read Level 3/5 #system-design#redis#ioredis
What you'll learn
  • Use Redis's core data structures from Node with ioredis
  • Apply pipelines, transactions, and pub/sub correctly
  • Map Redis patterns to caching, sessions, rate limiting, and leaderboards

Redis has shown up in every lesson of this section — as the shared session store, the cache, the pub/sub bus, the lock. It’s the Swiss Army knife of the scaling toolkit, and for Node engineers it’s the single most useful piece of infrastructure to know cold. This lesson goes deep on what Redis actually is and how you wield it from Node with ioredis, the de-facto Node client.

Redis is best understood not as a cache but as an in-memory data structure server: it keeps your data in RAM (so it’s microsecond-fast) and exposes it not as opaque blobs but as real data structures — strings, hashes, lists, sets, sorted sets — each with rich, atomic operations. That structure is what makes one tool serve so many roles.

Why single-threaded is a feature, not a bug

Redis executes commands on one thread, one at a time. That sounds like a limitation until you realize what it buys you: every command is atomic. There are no locks, no race conditions, no half-applied operations — while INCR runs, nothing else can interleave with it. A counter incremented by a thousand concurrent clients lands at exactly the right number, for free.

And it’s fast because it’s simple: no lock contention, no context-switching between threads, no coordination overhead. Everything is in RAM and the single thread just rips through commands. This is the same lesson the Node event loop teaches — a single thread doing non-blocking work can outperform a thread pool fighting over locks. Redis and Node are spiritual siblings.

The core data structures

Each Redis type maps to a system-design job. Here’s the working vocabulary, with ioredis calls.

TypeThink of it asClassic use
StringA value (text, number, JSON, bytes)Cache entry, counter, flag
HashAn object / Map of fieldsSession, user record
ListA linked list / dequeQueue, recent-items feed
SetA unique, unordered collectionTags, “who liked this”, dedup
Sorted SetA set where each member has a scoreLeaderboard, rate limiter, priority queue
The five core types from ioredis script.js
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);

// String — values & atomic counters
await redis.set('page:home', html, 'EX', 60);     // cache w/ TTL
await redis.incr('views:home');                    // atomic ++

// Hash — store an object without serializing the whole thing
await redis.hset('user:42', { name: 'Ada', plan: 'pro' });
await redis.hget('user:42', 'plan');               // 'pro'

// List — a queue (push one end, pop the other)
await redis.lpush('jobs', JSON.stringify({ task: 'email' }));
const job = await redis.rpop('jobs');              // FIFO

// Set — uniqueness & membership
await redis.sadd('post:9:likes', 'u1', 'u2', 'u1'); // u1 counted once
await redis.scard('post:9:likes');                  // 2 unique likers

// Sorted Set — members ranked by score
await redis.zadd('leaderboard', 5000, 'Ada', 4200, 'Lin');
await redis.zrevrange('leaderboard', 0, 2, 'WITHSCORES'); // top 3
▶ Preview: console

Pipelines and transactions

Each Redis command is a network round trip (~0.5ms in-datacenter). Fire a hundred commands one at a time and you pay a hundred round trips serially. Pipelining batches them into a single round trip — the commands still execute in order, you just send and receive once.

A transaction (MULTI/EXEC) goes further: it groups commands so they run atomically as a unit, with nothing else interleaved between them. In ioredis, .multi() both pipelines and wraps the batch in MULTI/EXEC.

Pipeline for speed, multi for atomicity script.js
// PIPELINE — one round trip instead of three. Faster, not atomic.
const results = await redis.pipeline()
  .set('a', 1)
  .incr('a')
  .get('a')
  .exec();                         // [[null,'OK'], [null,2], [null,'2']]

// MULTI/EXEC — atomic: these run as an indivisible unit.
// Useful when several writes must all land together or not at all.
await redis.multi()
  .incr('user:42:posts')
  .sadd('active:today', '42')
  .expire('active:today', 86400)
  .exec();
▶ Preview: console

For conditional atomic logic (“increment only if under the limit”), you’d reach for a Lua script via redis.eval(...), which runs server-side as one atomic operation — the standard trick for correct rate limiters.

Pub/Sub

Redis can also be a message bus: clients SUBSCRIBE to channels, others PUBLISH to them, and every subscriber gets every message. This is fire-and-forget (no persistence, no delivery guarantee) — it’s how the socket.io Redis adapter from the statelessness lesson lets every app instance broadcast to every connected client.

Pub/Sub across instances script.js
// A subscriber connection must be dedicated — it can't run normal commands.
const sub = new Redis(process.env.REDIS_URL);
const pub = new Redis(process.env.REDIS_URL);

sub.subscribe('chat:room1');
sub.on('message', (channel, message) => {
  console.log(`[${channel}] ${message}`);    // every instance receives this
});

await pub.publish('chat:room1', JSON.stringify({ from: 'Ada', text: 'hi' }));
▶ Preview: console

Four patterns you’ll reach for constantly

The data structures above compose into a handful of go-to system-design patterns:

1. Cache. Strings with TTLs — the cache-aside pattern from two lessons back. GET/SET key value EX seconds.

2. Session store. A hash per session (or a serialized string), reachable by every stateless instance — the connect-redis setup from the statelessness lesson.

3. Rate limiter. A counter per user per window, incremented atomically and expired automatically — the single-threaded atomicity makes this correct under any concurrency:

A fixed-window rate limiter script.js
// Allow `limit` requests per `windowSec` per user. Atomic and lock-free.
async function allow(userId, limit = 100, windowSec = 60) {
  const key = `rl:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`;
  const count = await redis.incr(key);          // atomic ++
  if (count === 1) await redis.expire(key, windowSec); // set TTL on first hit
  return count <= limit;                          // false → rate limited
}
▶ Preview: console

4. Leaderboard. A sorted set — members ranked by score, with O(log N) updates and instant top-N and rank queries. This is the canonical Redis showcase:

A real-time leaderboard with sorted sets script.js
// Record a score (ZADD updates in place if the member exists).
await redis.zadd('game:scores', 9001, 'player:7');

// Top 10, highest first, with scores.
const top10 = await redis.zrevrange('game:scores', 0, 9, 'WITHSCORES');

// A specific player's rank (0-based) — O(log N), no full scan.
const rank = await redis.zrevrank('game:scores', 'player:7');
console.log(`You are #${rank + 1}`);
▶ Preview: console

Each of these is a few lines because Redis ships the right data structure and the single-threaded guarantee that makes concurrent access correct. That combination — the right structures, atomic by construction, microsecond-fast — is why Redis is the backbone of nearly every scaling pattern in this track.

That closes out the Scaling Building Blocks. You can now spread load across many stateless servers, front them with proxies and CDNs, and cache aggressively with Redis. Next we go down a layer to where the durable truth lives — choosing between SQL and NoSQL for your data.