Redis Beyond Caching: Pub/Sub, Streams, and Time Series Data

Redis Is More Than a Cache

Most developers know Redis as a fast key-value cache. Set a key, get a key, set an expiration. And that's a great use case — Redis handles millions of operations per second with sub-millisecond latency. But treating it as just a cache means you're ignoring some genuinely useful data structures and features.

Pub/Sub for Real-Time Features

Redis Pub/Sub is the simplest way to add real-time messaging between services or to clients. It's not a message queue (messages are fire-and-forget — if nobody's listening, the message is lost), but for use cases where that's acceptable, it's hard to beat the simplicity.

JavaScript Runtime Comparison
Runtime Cold Start HTTP Throughput TypeScript
Node.js 22~50 ms~85k req/sVia tsx/ts-node
Deno 2~30 ms~120k req/sBuilt-in
Bun 1.x~10 ms~260k req/sBuilt-in
Cloudflare Workers0 ms (pre-warm)N/A (edge)Via Wrangler
# Publisher
import redis
r = redis.Redis()
r.publish('notifications', '{"user_id": 42, "type": "order_shipped"}')

# Subscriber
pubsub = r.pubsub()
pubsub.subscribe('notifications')
for message in pubsub.listen():
 if message['type'] == 'message':
 handle_notification(json.loads(message['data']))

Common uses: invalidating caches across multiple app servers, pushing real-time updates to WebSocket connections, broadcasting configuration changes.

The limitation: Pub/Sub doesn't persist messages. If your subscriber disconnects and reconnects, it misses everything published during the gap. For durable messaging, you want Redis Streams.

See also: Server-Side Rendering Deep Dive: Streaming, Hydration, and I.

Redis Streams: Event Sourcing Made Simple

Streams, introduced in Redis 5.0, are append-only log data structures — think of them as a lightweight Kafka topic built into Redis.

# Add events to a stream
r.xadd('orders:events', {
 'type': 'order_created',
 'order_id': '12345',
 'amount': '9999',
 'customer': '[email protected]'
})

# Read events (consumer group for load balancing)
r.xgroup_create('orders:events', 'processor-group', id='0', mkstream=True)

# Each consumer in the group gets different messages
messages = r.xreadgroup(
 'processor-group', 'worker-1',
 {'orders:events': '>'},
 count=10, block=5000
)

# Acknowledge after processing
for stream, entries in messages:
 for entry_id, data in entries:
 process_order_event(data)
 r.xack('orders:events', 'processor-group', entry_id)

Streams give you:

  • Message persistence (survives restarts)
  • Consumer groups (multiple consumers sharing the workload)
  • Message acknowledgment (at-least-once delivery)
  • Historical replay (read from any point in the stream)
  • Automatic ID generation with timestamps

For small to medium scale event processing (under 100,000 events/second), Streams are a solid alternative to running a separate Kafka cluster. You get 80% of the functionality with 20% of the operational complexity.

For a related perspective, see Astro vs Next.js vs Remix: Choosing a Meta-Framework in 2026.

Time Series Data

Redis has a time series module (RedisTimeSeries) that's optimized for metrics, sensor data, and monitoring. If you're already running Redis, it can save you from deploying a separate time series database for smaller workloads.

# Create a time series key with retention and labels
r.ts().create('sensor:temp:living-room',
 retention_msecs=86400000, # Keep 24 hours
 labels={'room': 'living-room', 'type': 'temperature'})

# Add data points
r.ts().add('sensor:temp:living-room', '*', 22.5)
r.ts().add('sensor:temp:living-room', '*', 22.7)

# Aggregate queries
r.ts().range('sensor:temp:living-room',
 from_time='-',
 to_time='+',
 aggregation_type='avg',
 bucket_size_msec=3600000) # Hourly averages

For serious time series workloads (millions of data points per second, long retention), you'll want a dedicated database like TimescaleDB or InfluxDB. But for application metrics, IoT dashboards with a few hundred sensors, or real-time monitoring counters, RedisTimeSeries keeps things simple.

Other Useful Patterns

Sorted Sets for leaderboards and ranking:

We covered a related topic in Time Series Data: InfluxDB, TimescaleDB, and Handling High-C.

# Add scores
r.zadd('game:leaderboard', {'alice': 2500, 'bob': 1800, 'charlie': 3200})

# Top 10 players
r.zrevrange('game:leaderboard', 0, 9, withscores=True)

# Player's rank
r.zrevrank('game:leaderboard', 'bob') # Returns 2 (0-indexed)

HyperLogLog for unique counting: Approximate unique visitor counts using 12KB of memory regardless of cardinality. Accuracy within 0.81%.

# Count unique visitors
r.pfadd('visitors:2026-09-20', user_id)
unique_count = r.pfcount('visitors:2026-09-20')

Geospatial indexes: Store locations and query by distance.

r.geoadd('restaurants', -122.4194, 37.7749, 'joes-pizza')
# Find restaurants within 2km
r.geosearch('restaurants', longitude=-122.42, latitude=37.77,
 radius=2, unit='km')

When Not to Use Redis

Redis keeps everything in memory. A dataset that's 50GB means you need 50GB+ of RAM (plus replication). For large datasets, consider whether the data genuinely needs sub-millisecond access or if a disk-based solution would work fine with a small cache layer on top.

Also, Redis persistence (RDB snapshots and AOF) is good but not as durable as a proper database. Don't use Redis as your primary data store for data you can't afford to lose. Use it alongside your database, not instead of it.

References & Standards: Node.js documentation · W3C Standards