Redis caching & Redis Streams patterns — Architecture & Trade‑offs — Practical Guide (May 20, 2026)
Redis caching & Redis Streams patterns — Architecture & Trade‑offs
Level: Intermediate
Date: May 20, 2026
Introduction
Redis remains a go-to in-memory data structure store powering caching layers and event streaming systems alike. Its rich feature set—from simple key-value caching to Redis Streams introduced in version 5.0 (GA from v5)—allows architects to build responsive, resilient systems. This article explores practical patterns for Redis caching and Redis Streams, the trade-offs involved, and guidance on selecting approaches aligned to your application needs.
Prerequisites
- Redis version 6.2 or later recommended to ensure access to stable Streams features and enhanced data eviction policies.
- Basic understanding of caching concepts and publish-subscribe/messaging systems.
- Familiarity with Redis clients supporting Streams, such as
redis-py4.x,node-redis4.x, or Jedis 4.x. - Environment setup with Redis persistence (AOF/RDB) configured for durability, if using Streams for event/message storage.
Redis Caching Patterns
Redis caching is typically implemented as a layer between your application and primary datastore to reduce latency and load. Key patterns include:
1. Cache-Aside Pattern
Application code checks Redis first; on cache miss, it loads from database, then writes to Redis with an expiration.
// Pseudocode cache-aside example
const cacheKey = `user:${userId}`;
let userData = await redis.get(cacheKey);
if (!userData) {
userData = await db.fetchUser(userId);
if (userData) {
await redis.set(cacheKey, JSON.stringify(userData), { EX: 3600 }); // 1 hour TTL
}
}
return JSON.parse(userData);
2. Write-Through and Write-Back (Write-Behind)
Less common with Redis due to complexity and risk of data loss:
- Write-Through: writes go synchronously to cache and DB.
- Write-Back: writes are acknowledged by cache then asynchronously flushed to DB.
Carefully consider consistency and failure handling before using these.
3. Cache Invalidation Strategies
Keeping cache consistent is critical:
- Time-based TTLs: simple but may serve stale data.
- Event-driven invalidation: e.g., app triggers
DELor updated cache on writes. - Versioned Keys: embed version in keys to implicitly expire old entries.
Redis Streams Patterns
Redis Streams offer a log data structure for messaging, event sourcing, and real-time data feeds, supporting consumer groups, reliable delivery, and persistence.
1. Event Messaging with Consumer Groups
Use consumer groups to distribute message processing among multiple consumers with tracking via pending entries list (PEL).
# Simplified consumer group read loop with redis-py
group = "orders_group"
consumer = "worker_1"
stream = "orders_stream"
# Create group once (id '$' to start from messages added after group creation)
try:
redis.xgroup_create(stream, group, id="$", mkstream=True)
except redis.exceptions.ResponseError:
pass # Group already exists
while True:
resp = redis.xreadgroup(group, consumer, {stream: ">"}, count=10, block=5000)
if resp:
for stream_name, messages in resp:
for message_id, fields in messages:
process(fields)
redis.xack(stream, group, message_id)
2. Event Sourcing
Use Streams as an append-only log of changes. Carefully plan retention policies (maxlen or deletion) since Streams persist indefinitely by default.
3. Patterns for Reliable Delivery
Leverage:
- Pending entries lists (PEL) for tracking unacknowledged messages.
- Consumer auto-claiming of stuck messages after timeouts.
Architecture and Trade-offs
Cache vs Stream: When to Choose Each?
| Use Case | Redis Caching | Redis Streams |
|---|---|---|
| Reduce latency on read-heavy workloads | Ideal: fast, simple key-value or hash caching | Not suitable |
| Reliable event/message processing | Not designed for messaging features | Well-suited with consumer groups and PEL |
| Event sourcing or audit trails | No native append-only log semantics | Preferred; supports infinite, ordered streams |
| Data consistency and ordering | Cache may be stale or invalidated asynchronously | Offers strong ordering guarantees per stream |
Trade-offs
- Memory usage: Streams hold all messages unless trimmed; caching keys are usually time-limited.
- Complexity: Streams require more operational knowledge and robust consumers; caching logic can remain simple.
- Persistence & Durability: Streams benefit from configurable AOF/RDB; caches often rely on ephemeral TTL expiry.
- Failure handling: Streams support message reprocessing on failures via PEL and manual control.
Hands-on: Implementing a Simple Cache-Aside & Stream Consumer
This example pairs a cache-aside key-value caching pattern with a Redis Streams consumer that processes events and updates the cache.
// Node.js example with 'redis' v4+
import { createClient } from "redis";
const client = createClient();
await client.connect();
async function getUser(userId) {
const key = `user:${userId}`;
let cached = await client.get(key);
if (cached) {
return JSON.parse(cached);
}
const userFromDb = await fetchUserFromDb(userId); // Implement accordingly
if (userFromDb) {
await client.set(key, JSON.stringify(userFromDb), { EX: 3600 });
}
return userFromDb;
}
async function processUserUpdates() {
const stream = "user_updates";
const group = "update_consumers";
const consumerName = "consumer1";
try {
await client.xGroupCreate(stream, group, "$", { MKSTREAM: true });
} catch (e) {
if (!e.message.includes("BUSYGROUP")) throw e;
}
while (true) {
const response = await client.xReadGroup(group, consumerName, [{ key: stream, id: ">" }], { COUNT: 10, BLOCK: 5000 });
if (response) {
for (const { id, message } of response[0].messages) {
const { userId, name } = message;
// Update cache with new user data
await client.set(`user:${userId}`, JSON.stringify({ userId, name }), { EX: 3600 });
await client.xAck(stream, group, id);
}
}
}
}
// Kick off consumer
processUserUpdates().catch(console.error);
// Helper placeholder for DB fetch
async function fetchUserFromDb(userId) {
// Your DB call here
return { userId, name: "John Doe" };
}
Common Pitfalls
- Ignoring TTLs in caching: Missing expiry leads to stale data and memory bloat.
- Over-trimming Streams: Setting maxlen too low may cause lost events for slow consumers.
- Assuming ordering across multiple streams: Redis guarantees ordering per stream, not across.
- Not monitoring PEL size: Unacknowledged messages accumulate, increasing memory usage.
- Skipping consumer group creation errors: Handling “BUSYGROUP” response correctly avoids startup issues.
Validation
Verify your caching and streaming solutions by:
- Measuring cache hit/miss ratio and latency with tools like
redis-cli --latencyand application-level metrics. - Inspecting Redis Streams consumer groups with commands:
XINFO GROUPS mystream
XINFO CONSUMERS mystream mygroup
XLEN mystream
- Checking pending messages to identify stuck processing.
- Simulating failover or restarts to ensure message redelivery works properly.
- Monitoring Redis memory usage with