Node.js + Redis Streams in 2026: Event Streaming Without Kafka
product-development11 min readintermediate

Node.js + Redis Streams in 2026: Event Streaming Without Kafka

Vivek Singh
Founder & CEO at Witarist · June 9, 2026

Most Node.js teams reach for Kafka the moment someone says "event-driven," then spend the next quarter babysitting brokers, ZooKeeper or KRaft quorums, and partition rebalances. In 2026, a large share of those systems never needed that weight. If you already run Redis for caching or rate limiting, Redis Streams gives you a durable, append-only event log with native consumer groups — and you can ship it this week instead of next quarter.

This guide covers Redis Streams end to end for Node.js: how the append-only log and entry IDs work, producing with XADD, scaling consumers with XREADGROUP and consumer groups, and the reliability machinery — the Pending Entries List, XACK, XAUTOCLAIM, and an application-level dead-letter queue — that turns at-least-once delivery into something you can actually trust in production.

Why Redis Streams Matters in 2026

One dependency instead of three

Redis Streams ships inside the same Redis (or Valkey) instance you probably already operate. There is no separate cluster to provision, no schema registry, and no Connect framework. For teams using Redis for caching already, streams add durable messaging at near-zero marginal operational cost.

Durable, replayable, and ordered

Unlike Redis Pub/Sub, which drops messages when no one is listening, a stream is an append-only log. Entries persist until you trim them, every entry has a monotonically increasing ID, and new consumers can replay history from ID 0. That combination — durability plus ordering plus replay — is exactly what people leave Pub/Sub for and reach for Kafka to get.

Node.js Redis Streams pipeline showing XADD producers, an append-only stream log, a consumer group with XREADGROUP, and PEL recovery via XAUTOCLAIM
Figure 1 — The Redis Streams pipeline: producers append with XADD, a consumer group fans entries out across workers, and the PEL tracks anything not yet acknowledged.

How the Append-Only Log Works

Entry IDs and ordering

Each entry gets an ID of the form milliseconds-sequence, for example 1718000000000-0. IDs are strictly increasing, so the stream is totally ordered. Passing * to XADD lets Redis generate the timestamp-based ID for you, which is what you want in almost all cases — explicit IDs are reserved for migrations and tests.

Trimming keeps memory bounded

A stream grows forever unless you trim it. Use a capped MAXLEN with the approximate operator (MAXLEN ~ 1000000) so Redis can trim efficiently at radix-tree node boundaries rather than counting exact entries on every write. For time-based retention, MINID lets you drop everything older than a given ID.

ℹ️Note
Redis Streams give you at-least-once delivery, not exactly-once. A consumer can crash after processing but before XACK, so the same entry may be redelivered. Design handlers to be idempotent — key writes by the entry ID or a business idempotency key.
Figure 2 — Approximate single-node throughput ceilings. Redis Streams comfortably covers the vast majority of Node.js workloads without Kafka's operational weight.

Producing Events with XADD

Producing is a single command. With the modern node-redis client, an XADD call appends a field-value map and returns the generated entry ID. Wrap it in a thin helper so every producer trims consistently and you never forget MAXLEN.

producer.js
import { createClient } from 'redis';

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

// Append an event to the stream, auto-trimming to ~1M newest entries.
export async function publishOrderEvent(order) {
  const id = await redis.xAdd(
    'orders',                 // stream key
    '*',                      // let Redis assign the ID
    {                         // the entry payload (flat string map)
      type: 'order.created',
      orderId: String(order.id),
      amount: String(order.amountCents),
      ts: String(Date.now()),
    },
    { TRIM: { strategy: 'MAXLEN', strategyModifier: '~', threshold: 1_000_000 } }
  );
  return id; // e.g. "1718000000000-0"
}
Comparison table of Redis Streams versus Kafka, BullMQ, and RabbitMQ across setup effort, persistence, consumer groups, replay, throughput, and ops overhead
Figure 3 — Where Redis Streams fits among Node.js messaging options. It wins on setup effort and ops overhead while still offering durable, replayable, grouped consumption.

Consumer Groups: Scaling Out with XREADGROUP

Create the group, then read with >

Ready to build your team?

Hire Pre-Vetted Node.js Developers

Skip the months-long search. Our exclusive talent network has senior Node.js experts ready to join your team in 48 hours.

A consumer group lets multiple Node.js workers share a stream, with each entry delivered to exactly one consumer in the group. Create the group once with XGROUP CREATE (use MKSTREAM so it works before the first XADD), then each worker calls XREADGROUP with the special > ID to get never-before-delivered entries. This is the same fan-out model you would build a whole backend team around in Kafka, without the brokers.

worker.js
const GROUP = 'order-workers';
const CONSUMER = `worker-${process.pid}`;

// Idempotent group creation.
try {
  await redis.xGroupCreate('orders', GROUP, '0', { MKSTREAM: true });
} catch (err) {
  if (!String(err.message).includes('BUSYGROUP')) throw err;
}

async function runWorker() {
  while (true) {
    const res = await redis.xReadGroup(
      GROUP, CONSUMER,
      [{ key: 'orders', id: '>' }],   // '>' = only new, undelivered entries
      { COUNT: 10, BLOCK: 5000 }       // batch up to 10, block up to 5s
    );
    if (!res) continue;                // timed out, loop again
    for (const { messages } of res) {
      for (const { id, message } of messages) {
        try {
          await handleEvent(message);  // your business logic (idempotent!)
          await redis.xAck('orders', GROUP, id);  // remove from the PEL
        } catch (err) {
          // leave unacked: it stays in the PEL for XAUTOCLAIM to retry
          console.error('event failed', id, err);
        }
      }
    }
  }
}
Figure 4 — When a worker dies, its in-flight entries pile up in the Pending Entries List. After restart, XAUTOCLAIM reassigns and drains them.

Reliability: PEL, XACK, and XAUTOCLAIM

The Pending Entries List

When a consumer reads with >, Redis records the entry in the group's Pending Entries List (PEL) until that consumer calls XACK. The PEL is what makes delivery reliable: if a worker crashes mid-processing, the entry is still pending and can be reclaimed. But an unbounded PEL is a memory and latency problem — millions of unacked entries slow XREADGROUP down — so acknowledge promptly and monitor PEL size with XPENDING.

Reclaiming stale work and a dead-letter queue

Use XAUTOCLAIM on a timer to reassign entries that have been pending longer than your processing SLA (for example, idle > 60s) to a live consumer. Track each entry's delivery count from XPENDING; once it exceeds a threshold (say 5 attempts), it is a poison message — XACK it on the main stream and XADD it to an orders:dlq stream for out-of-band inspection. Redis has no built-in DLQ, so this pattern lives in your application code.

⚠️Warning
Never run a blocking XREADGROUP on a connection that other code shares. A BLOCK call holds the connection for its full timeout; if your pool is small, every other Redis operation queues behind it. Give stream workers their own dedicated client (or a duplicated connection).
🚀Pro Tip
Process entries in small COUNT batches and XACK each as soon as it succeeds, rather than acking a whole batch at the end. Partial-batch crashes then redeliver only the unacked tail, not the entries you already handled.

Redis Streams vs Kafka, BullMQ, and RabbitMQ

Reach for Kafka when you genuinely need multi-million-events-per-second throughput, long-term log retention measured in weeks, or an ecosystem of stream processors and connectors. For most Node.js services, that is over-provisioning. Redis Streams covers durable, ordered, grouped consumption at the scale typical APIs actually hit, with a fraction of the operational surface.

If your need is really a background job queue with retries, scheduling, and a dashboard, BullMQ (also Redis-backed) is a better fit than raw streams. RabbitMQ shines for complex routing topologies. The honest answer is usually "the lightest tool that meets your durability and throughput needs" — and getting that judgment right is exactly the kind of call an experienced engineer makes quickly. If you need that judgment on your team, you can hire pre-vetted Node.js developers through HireNodeJS.

Building an event-driven Node.js system and short on senior backend capacity? HireNodeJS connects you with pre-vetted senior engineers — fluent in Redis, streaming, and production reliability — available within 48 hours, without the recruiter overhead. See how it works.

Hire Expert Node.js Developers — Ready in 48 Hours

Building the right system is only half the battle — you need the right engineers to build it. HireNodeJS.com specialises exclusively in Node.js talent: every developer is pre-vetted on real-world projects, API design, event-driven architecture, and production deployments.

Unlike generalist platforms, our curated pool means you speak only to engineers who live and breathe Node.js. Most clients have their first developer working within 48 hours of getting in touch. Engagements start as short-term contracts and can convert to full-time hires with zero placement fee.

💡Tip
🚀 Ready to scale your Node.js team? HireNodeJS.com connects you with pre-vetted engineers who can join within 48 hours — no lengthy screening, no recruiter fees. Browse developers at hirenodejs.com/hire

Conclusion

Redis Streams gives Node.js teams a durable, ordered, replayable event log with native consumer groups — most of what people adopt Kafka for, on infrastructure many teams already run. Master the four moves (XADD to produce, XREADGROUP to consume in a group, XACK to clear the PEL, and XAUTOCLAIM plus a DLQ to recover) and you have a reliable at-least-once pipeline you can ship and operate without a dedicated streaming platform team.

Start small: one stream, one consumer group, idempotent handlers, and a PEL-size alert. Scale workers horizontally as load grows, and graduate to Kafka only if and when your throughput or retention genuinely outgrows Redis. For the vast majority of 2026 Node.js workloads, that day never comes.

Topics
#Node.js#Redis#Redis Streams#Event Streaming#Consumer Groups#Message Queues#Backend

Frequently Asked Questions

What are Redis Streams and how do they differ from Pub/Sub?

Redis Streams are a durable, append-only log data type with ordered entry IDs and native consumer groups. Unlike Pub/Sub, entries persist, can be replayed from any ID, and survive consumers being offline.

Do Redis Streams guarantee exactly-once delivery?

No. Redis Streams provide at-least-once delivery via the Pending Entries List. A consumer can crash after processing but before XACK, causing redelivery, so your handlers must be idempotent.

When should I use Redis Streams instead of Kafka?

Use Redis Streams when you already run Redis and need durable, ordered, grouped consumption at typical API scale. Choose Kafka only for multi-million events per second, long log retention, or a stream-processing ecosystem.

How do I prevent the Pending Entries List from growing unbounded?

Acknowledge entries promptly with XACK, process in small COUNT batches, monitor PEL size with XPENDING, and run XAUTOCLAIM on a timer to reclaim stale entries from dead consumers.

How do I handle poison messages in Redis Streams?

Track each entry's delivery count from XPENDING. Once it exceeds a threshold, XACK it on the main stream and XADD it to a separate dead-letter stream (e.g. orders:dlq), since Redis has no built-in DLQ.

Can I run multiple Node.js workers on one stream?

Yes. Create a consumer group with XGROUP CREATE, then have each worker call XREADGROUP with a unique consumer name and the > ID. Each entry is delivered to exactly one worker in the group.

About the Author
Vivek Singh
Founder & CEO at Witarist

Vivek Singh is the founder of Witarist and HireNodeJS.com — a platform connecting companies with pre-vetted Node.js developers. With years of experience scaling engineering teams, Vivek shares insights on hiring, tech talent, and building with Node.js.

Developers available now

Need a Node.js engineer fluent in Redis & event streaming?

HireNodeJS connects you with pre-vetted senior Node.js engineers experienced in Redis, streaming, and production reliability — available within 48 hours. No recruiter fees, no lengthy screening.