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.
# 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.
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': 'jane@example.com'
})
# 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.
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:
# 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.