Node.js + Redis Streams in 2026: Event Streaming Without Kafka
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.

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.
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.
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"
}
Consumer Groups: Scaling Out with XREADGROUP
Create the group, then read with >
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.
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);
}
}
}
}
}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.
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.
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.
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.
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.
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.
