Sachith Dassanayake Software Engineering Redis caching & Redis Streams patterns — Architecture & Trade‑offs — Practical Guide (May 20, 2026)

Redis caching & Redis Streams patterns — Architecture & Trade‑offs — Practical Guide (May 20, 2026)

Redis caching & Redis Streams patterns — Architecture & Trade‑offs — Practical Guide (May 20, 2026)

Redis caching & Redis Streams patterns — Architecture & Trade‑offs

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-py 4.x, node-redis 4.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 DEL or 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 --latency and 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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Post