edit_document// BLOG_POST.md

Redis Beyond Caching: Queues, Pub/Sub, Rate Limiting, and Session Storage

//

, ,

Most developers know Redis as a cache: store a JSON blob, set a TTL, avoid hitting the database on every request. That alone is valuable, but it barely scratches the surface. Redis is an in-memory data structure server that supports strings, lists, sets, sorted sets, hashes, streams, and hyperloglogs. Each data structure enables a class of problems that would otherwise require a dedicated system. One Redis instance can replace your cache, your job queue, your rate limiter, your session store, and your real-time event bus.

Pattern 1: Job Queues with Redis Lists

Redis lists with LPUSH and BRPOP create a reliable FIFO queue. The producer pushes jobs to the left; workers block-pop from the right:

import Redis from 'ioredis';
const redis = new Redis();

// Producer: enqueue a job
async function enqueue(queue: string, job: Record<string, unknown>) {
  const payload = JSON.stringify({ id: crypto.randomUUID(), data: job, createdAt: Date.now() });
  await redis.lpush(queue, payload);
}

// Worker: process jobs with blocking pop
async function startWorker(queue: string, handler: (job: any) => Promise<void>) {
  const conn = new Redis(); // Separate connection for blocking ops
  while (true) {
    const result = await conn.brpop(queue, 30);
    if (!result) continue;
    const [, payload] = result;
    const job = JSON.parse(payload);
    try {
      await handler(job);
    } catch (err) {
      await redis.lpush(`${queue}:failed`, payload); // Dead letter queue
    }
  }
}

await enqueue('emails', { to: 'user@example.com', template: 'welcome' });
startWorker('emails', async (job) => console.log(`Sending to ${job.data.to}`));

For production job queues with retries, delays, priorities, and dashboards, use BullMQ, which builds on this same Redis foundation.

Pattern 2: Rate Limiting with Sorted Sets

The sliding window rate limiter uses a sorted set where the score is the timestamp:

async function isRateLimited(
  redis: Redis, key: string, limit: number, windowMs: number
): Promise<{ allowed: boolean; remaining: number }> {
  const now = Date.now();
  const windowStart = now - windowMs;

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);
  pipeline.zadd(key, now.toString(), `${now}-${Math.random()}`);
  pipeline.zcard(key);
  pipeline.pexpire(key, windowMs);

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

  if (count > limit) {
    await redis.zremrangebyscore(key, now, now);
    return { allowed: false, remaining: 0 };
  }
  return { allowed: true, remaining: limit - count };
}

// 100 requests per minute per IP
const result = await isRateLimited(redis, `rate:${ip}`, 100, 60_000);

Pattern 3: Pub/Sub for Real-Time Events

Redis Pub/Sub lets services broadcast events without knowing who is listening. Fire-and-forget messaging for cache invalidation, notifications, and microservice communication:

// Publisher
await redis.publish('orders', JSON.stringify({
  type: 'order.placed', orderId: 'ord_abc123', total: 49.99,
}));

// Subscriber
const sub = new Redis();
sub.subscribe('orders', 'inventory');
sub.on('message', (channel, message) => {
  const event = JSON.parse(message);
  if (channel === 'orders' && event.type === 'order.placed') {
    console.log(`New order: ${event.orderId}`);
  }
});

Pub/Sub messages are not persisted. If a subscriber is offline, it misses messages. For guaranteed delivery, use Redis Streams with consumer groups.

Pattern 4: Session Storage

Storing sessions in Redis enables stateless application servers. Any instance can serve any user because session data lives in shared Redis:

import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000,
    sameSite: 'lax',
  },
}));

Pattern 5: Leaderboards with Sorted Sets

await redis.zadd('leaderboard', 1500, 'player:alice');
await redis.zadd('leaderboard', 2300, 'player:bob');
await redis.zincrby('leaderboard', 100, 'player:alice');

// Top 10 (highest first)
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// ['player:bob', '2300', 'player:alice', '1600']

// Player rank (0-indexed)
const rank = await redis.zrevrank('leaderboard', 'player:alice'); // 1

Sorted sets maintain order automatically. Adding, updating, and ranking are all O(log n). A million-player leaderboard responds in microseconds.

When Redis Is Not the Answer

Redis stores everything in memory. Data larger than available RAM needs a different solution. Redis persistence (RDB, AOF) provides durability, but it is not a replacement for a primary database. Use Redis for fast, ephemeral, or auxiliary data patterns layered on top of your persistent storage.

Further reading: Redis Documentation | Redis Data Types | BullMQ Documentation


arrow_circle_right// POST_NAVIGATION

forum// COMMENTS

Leave a Reply

Your email address will not be published. Required fields are marked *