Redis has two messaging systems: Pub/Sub for fire-and-forget real-time events, and Streams for persistent, replayable event logs with consumer groups.

Pub/Sub

Pub/Sub is simple: publishers send messages to channels, subscribers receive them. Messages are not stored — if no subscriber is listening, the message is lost.

# Terminal 1: Subscribe to a channel
SUBSCRIBE notifications:user:1

# Terminal 2: Publish
PUBLISH notifications:user:1 '{"type":"message","text":"Hello!"}'
# Terminal 1 receives: "Hello!"

Node.js Pub/Sub

ioredis requires separate client connections for publishing and subscribing:

import Redis from "ioredis";

// Publisher client (normal connection)
const publisher = new Redis();

// Subscriber client (dedicated — can only subscribe)
const subscriber = new Redis();

// Subscribe
await subscriber.subscribe("notifications");
subscriber.on("message", (channel: string, message: string) => {
  const data = JSON.parse(message);
  console.log(`[${channel}]`, data);
});

// Publish from anywhere
async function sendNotification(userId: number, text: string) {
  await publisher.publish("notifications", JSON.stringify({
    userId,
    text,
    timestamp: Date.now(),
  }));
}

Pattern subscribe (multiple channels with glob):

await subscriber.psubscribe("notifications:*");
subscriber.on("pmessage", (pattern, channel, message) => {
  console.log(pattern, channel, JSON.parse(message));
});

Pub/Sub Limitations

  • Messages are not persisted — if your subscriber is offline, it misses messages
  • No consumer groups — all subscribers receive every message
  • No acknowledgement — you cannot confirm delivery

For these requirements, use Redis Streams.

Redis Streams

Streams are persistent, append-only logs. Consumers can replay messages from any point in history. Consumer groups allow parallel processing with acknowledgement.

# Add to stream (auto-generates ID: timestamp-sequence)
XADD orders * event "order_placed" order_id "123" amount "99.99"
# Returns: "1726500000000-0"

# Read from stream
XREAD COUNT 10 STREAMS orders 0

# Read new messages only ($ = latest)
XREAD COUNT 10 BLOCK 0 STREAMS orders $

Consumer Groups

Consumer groups allow multiple consumers to process the stream in parallel, each getting different messages:

# Create consumer group (starting from the beginning)
XGROUP CREATE orders processing-group 0 MKSTREAM

# Consumer reads from group
XREADGROUP GROUP processing-group worker1 COUNT 1 STREAMS orders >
# > means "give me new, undelivered messages"

# Acknowledge after processing
XACK orders processing-group 1726500000000-0

Node.js Streams Example

import Redis from "ioredis";

const redis = new Redis();

// Producer: add events to stream
async function placeOrder(orderId: string, amount: number) {
  const id = await redis.xadd("orders", "*",
    "event", "order_placed",
    "order_id", orderId,
    "amount", String(amount)
  );
  console.log("Event added:", id);
}

// Consumer: process events from consumer group
async function startWorker(workerId: string) {
  const GROUP = "processing-group";
  const STREAM = "orders";

  // Create group if it doesn't exist
  await redis.xgroup("CREATE", STREAM, GROUP, "0", "MKSTREAM").catch(() => {});

  console.log(`Worker ${workerId} started`);

  while (true) {
    // Read up to 1 message, block 2 seconds if empty
    const results = await redis.xreadgroup(
      "GROUP", GROUP, workerId,
      "COUNT", "1",
      "BLOCK", "2000",
      "STREAMS", STREAM, ">"
    ) as any;

    if (!results) continue;

    for (const [stream, messages] of results) {
      for (const [id, fields] of messages) {
        const data = Object.fromEntries(
          fields.reduce((acc: any[], v: string, i: number) =>
            i % 2 === 0 ? [...acc, [v, fields[i + 1]]] : acc, [])
        );

        console.log(`Processing ${id}:`, data);

        try {
          await processOrder(data);
          await redis.xack(STREAM, GROUP, id);
        } catch (err) {
          console.error("Processing failed:", err);
          // Message stays in pending list — will be retried or reassigned
        }
      }
    }
  }
}

async function processOrder(data: Record<string, string>) {
  // Your business logic here
  console.log("Order processed:", data.order_id);
}

Python Streams Example

import redis
import time
import json

r = redis.Redis(host="localhost", decode_responses=True)

# Producer
def place_order(order_id: str, amount: float):
    entry_id = r.xadd("orders", {
        "event": "order_placed",
        "order_id": order_id,
        "amount": str(amount),
    })
    print(f"Event added: {entry_id}")

# Consumer worker
def start_worker(worker_id: str):
    group = "processing-group"
    stream = "orders"

    # Create group if needed
    try:
        r.xgroup_create(stream, group, id="0", mkstream=True)
    except redis.exceptions.ResponseError:
        pass  # Group already exists

    print(f"Worker {worker_id} started")

    while True:
        # Read up to 1 message, block 2 seconds
        results = r.xreadgroup(group, worker_id, {stream: ">"}, count=1, block=2000)

        if not results:
            continue

        for stream_name, messages in results:
            for entry_id, data in messages:
                print(f"Processing {entry_id}:", data)
                try:
                    # Process the event
                    print(f"Order processed: {data['order_id']}")
                    r.xack(stream_name, group, entry_id)
                except Exception as e:
                    print(f"Error: {e}")

Pub/Sub vs Streams

Pub/SubStreams
PersistenceNo — messages lost if no subscriberYes — messages stored in Redis
Consumer groupsNo — all subscribers get all messagesYes — parallel processing
Message historyNoYes — replay from any point
AcknowledgementNoYes — XACK
Best forReal-time notifications, live updatesOrder processing, audit logs, queues

Use Pub/Sub for live chat or dashboard updates where message loss is acceptable. Use Streams for order processing, payment events, or any case where every message must be handled.

What’s Next?

You can now build real-time and event-driven systems with Redis. Next: Redis best practices for production — memory management, persistence, and monitoring.

Next: Database Tutorial #16: Redis Best Practices and Production