Redis Streams vs Kafka: How the Two Streaming Models Really Compare
When people pit "Redis vs Kafka" against each other, the comparison is usually unfair — Redis is a general-purpose data store and Kafka is a streaming platform, so most of the comparison is apples to oranges. There's one exception, and it's the only part of Redis that goes head-to-head with Kafka's core design: Redis Streams.
Both are append-only logs. Both have consumer groups. Both let you replay from a position. So this is the comparison worth taking seriously, and it's the one a growing number of teams resolve in Redis's favor — not because Redis Streams beats Kafka at everything, but because for a microservices event bus at realistic volume, it's often the lighter, faster, good-enough answer. This article goes deep on where the two models line up, where they diverge, how to tell which one your system actually needs — and where a Java client can take Redis Streams past its raw limits.
Why Streams Is the Real Comparison
Redis has three ways to move messages: built-in Pub/Sub (fire-and-forget, no persistence), Lists (simple queues), and Streams. Only Streams is built like Kafka — an ordered, append-only log of entries that multiple consumers can read independently, with acknowledgments and the ability to re-read history.
Kafka's whole model is that log: a topic is split into partitions, each partition is an immutable ordered sequence of records, and consumers track their position with offsets. Redis Streams gives you the same shape — an ordered sequence of entries, each with an ID, read via consumer groups that track what's been delivered and acknowledged. The concepts map cleanly onto each other, which is exactly why this is the comparison that rewards a close look.
Redis Streams vs Kafka at a Glance
| Redis Streams | Kafka | |
|---|---|---|
| Log structure | Single stream per key, ordered by entry ID | Topic split into partitions, each ordered by offset |
| Position tracking | Entry IDs (timestamp-sequence) | Offsets per partition |
| Consumer groups | Yes (XREADGROUP, PEL, XACK) | Yes (offset commits, rebalancing) |
| Parallelism | Manual sharding across streams/keys | Built-in via partitions |
| Ordering | Total order within a stream | Ordered within a partition |
| Delivery | At-least-once; app handles dedup | At-least-once; exactly-once available |
| Persistence | In-memory, with RDB/AOF; memory-bound | Disk-based, replicated; long retention |
| Retention | Trim by length (MAXLEN) or ID (MINID) | Time- and size-based; log compaction |
| Replay | Re-read from any retained ID | Seek to any offset within retention |
| Latency | Sub-millisecond | A few milliseconds, tunable |
| Throughput ceiling | Bound by Redis instance/cluster | Scales to millions/sec across brokers |
| Operational weight | Light | Heavier (brokers, partitions) |
The Data Model: Log vs Log
The first real difference is partitioning. A Kafka topic is partitioned out of the box, and partitions are the unit of parallelism — more partitions, more consumers reading in parallel, more throughput. Ordering is guaranteed within a partition, so Kafka routes messages with the same key to the same partition to keep related events in order.
A Redis stream, by contrast, is a single ordered log under one key. You get a clean total ordering for free, but parallelism doesn't come automatically: to spread load across a Redis Cluster you shard your data across multiple streams or keys yourself, and ordering only holds within each individual stream. For many event buses that's fine — you often want per-entity ordering, which a stream-per-entity or hash-tagged keys give you naturally. But if your scaling story depends on dozens of partitions consumed in parallel with automatic assignment, Kafka does that work for you and Redis asks you to design it.
Consumer Groups, Side by Side
This is where the two feel most similar and the details matter most.
In Redis Streams, you create a consumer group with XGROUP CREATE, and consumers read new entries with XREADGROUP. Each delivered-but-unacknowledged entry sits in the group's pending entries list (PEL) until the consumer calls XACK. If a consumer dies mid-processing, its entries stay in the PEL, and another consumer can take them over with XCLAIM or XAUTOCLAIM after a configurable idle time. That's a complete at-least-once mechanism with explicit, per-message acknowledgment.
Kafka's consumer groups assign whole partitions to consumers and track a committed offset per partition. Acknowledgment is implicit and positional: committing offset N means "everything up to N is done." That's efficient, but it also means you can't easily acknowledge message 5 as failed while 6 and 7 succeed — the offset is a single high-water mark. And when a consumer joins or leaves, Kafka rebalances partition assignments, which can briefly pause consumption across the group.
The practical contrast: Redis Streams gives you fine-grained, per-entry acknowledgment and reclaiming, with no rebalancing dance. Kafka gives you automatic partition assignment and battle-tested scaling, at the cost of coarse positional acks and rebalance pauses.
Delivery Guarantees
Both default to at-least-once. In Redis Streams, an unacknowledged entry will be redelivered (via claim) so nothing is silently lost, but duplicates are possible — your consumers need to be idempotent or dedupe on their own. Kafka is the same by default, and additionally offers exactly-once semantics through idempotent producers and transactions — though that guarantee is narrower than it sounds: it applies cleanly to Kafka-to-Kafka pipelines and carries real configuration and transactional overhead. If exactly-once is a hard requirement, raw Redis Streams puts that work on you — but, as the Redisson section below shows, a deduplication-based exactly-once is often simpler to reach than Kafka's transactional version.
Retention, Persistence, and Replay
This is the sharpest divergence, and it follows directly from where the data lives.
Kafka writes to disk and replicates across brokers, then retains by time or size — days, weeks, or effectively forever with log compaction or tiered storage. Replay means seeking to any offset still inside that window, which can be very large. This is what makes Kafka a genuine system of record for event sourcing.
Redis Streams live in memory. You persist with RDB snapshots and the append-only file, and you cap growth by trimming — XADD ... MAXLEN to bound the length, or XTRIM ... MINID to drop entries older than an ID (and since IDs are timestamps, that approximates time-based retention). You can replay by re-reading from any retained ID, including from the very beginning, but "retained" is bounded by RAM. Redis Streams is an excellent recent-history log; it is not the place to keep six months of events for a future consumer to replay. If long-horizon, large-volume replay is the requirement, that's Kafka.
Performance: Latency vs Throughput
Don't collapse these into one axis. Redis Streams, being in-memory, delivers sub-millisecond latency — hard to beat when you need events to land now. Kafka's latency is typically a few milliseconds (it's writing to and reading from disk, often in batches) and is tunable lower, but its real superpower is sustained throughput: it moves millions of durably stored records per second by scaling partitions across brokers. Redis Streams sustains very high throughput too, but its ceiling is tied to your Redis deployment rather than a horizontally partitioned broker fleet. Lowest latency on realistic volume points to Streams; maximal durable throughput points to Kafka.
The Operational Reality
It's easy to under-weight this until you're the one on call. A Redis instance — which many teams already run for caching — is a single, well-understood process; clustering adds shards but not a new mental model. Kafka is brokers, partitions, replication factors, in-sync replicas, and consumer-group coordination. Modern Kafka on KRaft removed the old ZooKeeper dependency, which helps, but it remains materially more to operate. If you're weighing Streams against Kafka purely to add an event bus to a stack that already has Redis, the operational delta is a real part of the decision, not a footnote.
When Redis Streams Is Enough — and When You Still Want Kafka
Reach for Redis Streams when you want a lightweight, low-latency event log or work stream, you already run Redis, your volume is in the thousands to low-tens-of-thousands per second, you mainly care about recent history, and per-entity ordering plus per-message acks fit your model better than automatic partition scaling.
Reach for Kafka when you need a durable system of record you can replay over long horizons, you're genuinely in the high-throughput / large-retention regime, you depend on the surrounding ecosystem (Kafka Connect, Schema Registry, Kafka Streams, ksqlDB), or you need exactly-once across a multi-stage pipeline.
Most microservice event buses sit comfortably in the first bucket — which is why "we chose Redis Streams over Kafka" has become a common, defensible decision rather than a contrarian one.
Closing the Gaps With Redisson
If you're building this on the JVM, you don't want to hand-roll XREADGROUP, PEL management, and XAUTOCLAIM loops. Redisson — one of the most widely used Redis and Valkey clients on the JVM, and production infrastructure for over a decade — wraps Redis Streams in an idiomatic Java interface through its RStream API (Community Edition): adding entries, creating groups, reading, acknowledging, inspecting and claiming pending entries, across synchronous, asynchronous, reactive, and RxJava3 styles. The same XGROUP/XREADGROUP/XACK semantics, without the raw commands or manual pending-list bookkeeping:
RStream<String, String> stream = redisson.getStream("orders");
stream.createGroup(StreamCreateGroupArgs.name("workers").id(StreamMessageId.NEWEST));
// Produce
stream.add(StreamAddArgs.entry("orderId", "A-1001"));
// Consume as a group member — Redisson tracks the pending list for you
Map<StreamMessageId, Map<String, String>> batch =
stream.readGroup("workers", "consumer-1", StreamReadGroupArgs.neverDelivered());
for (StreamMessageId id : batch.keySet()) {
process(batch.get(id));
stream.ack("workers", id); // unacknowledged entries can be reclaimed later
}
That removes most of the boilerplate while keeping the raw Streams semantics.
But RStream doesn't change the two real limitations of raw Streams: no native exactly-once, and manual sharding for parallelism. That's where Redisson's Reliable PubSub comes in, and it answers both directly. For the missing exactly-once, it adds deduplication-based exactly-once delivery with individual message acknowledgment, a native dead-letter queue, visibility timeouts, and delivery-attempt limits. For the manual sharding, it assigns consumers automatically — no hand-rolled partitioning and no rebalancing pauses. On top of that come configurable retention modes, per-operation synchronous replication, and message priorities and delays. In short, it offers the Kafka-grade guarantees teams worry about losing when they choose Redis — without the broker cluster.
What makes that trustworthy rather than best-effort is where the guarantees are enforced. Each step — claiming a message, acknowledging it, retrying, dead-lettering, deduplicating — runs as an atomic Lua script on the Redis or Valkey node itself, so it completes in full or not at all. A consumer crash or a competing reader can't leave the stream half-updated, and there's no separate broker to deploy and keep alive: the reliability rides on infrastructure you already operate.
If your event bus sits in Streams' sweet spot but you need those guarantees, the fastest way to find out is to run your own traffic through it — Redisson PRO offers a free trial.
Two deeper comparisons are worth reading next:
- Reliable PubSub vs Redis Streams — what Reliable PubSub adds on top of the raw Streams primitive.
- Apache Kafka vs Reliable PubSub — the full feature-by-feature head-to-head with Kafka.
And for the broader picture, see Redis vs Kafka for the overall tool decision, or Redis vs RabbitMQ if a traditional broker is also on your shortlist.