Redis Streams is a data structure specifically designed for message queue and event sourcing use cases. It addresses all the Problems of BLMOVE while remaining lightweight and fast.
1.1.
Key Features
Unique IDs. Every message has a globally unique, auto-generated ID
Structured data. Messages can have multiple field-value pairs
Consumer groups. Built-in support for distributed consumption
ACK mechanism. Native acknowledgment with pending message tracking
Range queries. Query messages by ID or timestamp
Persistence. Messages stay in stream until explicitly deleted
access. Fast lookup by message ID
1.2.
Basic Stream Operations
Adding messages with XADD.
1# Syntax: XADD stream_name ID field value [field value ...]2# * means "auto-generate ID"34XADD orders * orderID 1001 userID 123 amount 99.99 productID 4565# Returns: "1709251200000-0"67XADD orders * orderID 1002 userID 456 amount 149.50 productID 7898# Returns: "1709251200001-0"910XADD orders * orderID 1003 userID 123 amount 49.99 productID 32111# Returns: "1709251200002-0"
Understand Auto-generated IDs.
The ID format is TIMESTAMP-SEQUENCE:
TIMESTAMP: Milliseconds since Unix epoch
SEQUENCE: Counter starting from 0 for messages in same millisecond
1# ID: 1709251200000-02# └─timestamp─┘ └sequence3#4# If multiple messages added in same millisecond:5# 1709251200000-06# 1709251200000-1 7# 1709251200000-28# 1709251200001-0 (next millisecond)
List All Messages. To list all messages in a stream:
1XRANGE orders - +
In redis stream the notation - means the minimum possible and + means the maximum possible.
Redis Streams use a Radix Tree (also called compressed prefix tree) as the underlying data structure. This is why Streams are efficient for both insertion and range queries.
2.1.
What is a Radix Tree?
A Radix Tree is a space-optimized tree where nodes with single children are merged with their parent. It's particularly efficient for storing data with common prefixes.
Regular Trie vs Radix Tree:
1Regular Trie (stores "test", "team", "toast", "tear"):
2 root
3 / \
4 t ...
5 |
6 e
7 / \
8 s a
9 | |\
10 t m r
1112Radix Tree (same data, compressed):
13 root
14 /
15 t
16 /|\
17 est eam oast ear
2.2.
How Redis Uses Radix Trees for Streams
Redis Streams store messages in a Radix Tree where:
1# Fast time-range queries (common use case)2XRANGE events 170925120000017092512000013# Tree finds start node, iterates to end node4# O(log N + M) where M = 2 messages56# Fast "latest N messages" queries 7XREVRANGE events + - COUNT 1008# Start from rightmost node, iterate left 100 times9# Much faster than scanning entire list1011# Fast specific message lookup12XRANGE events 1709251200001-0 1709251200001-0
13# Direct tree traversal to node14# O(log N) instead of O(N) scan
3. Simple Payment Queue with Redis Streams
Let's build a practical payment processing queue using Redis Streams to see how it works in a real scenario.
1# Add messages2XADD orders:payments * orderID 5001 amount 1003XADD orders:payments * orderID 5002 amount 2004XADD orders:payments * orderID 5003 amount 30056# Consumer 1 reads all messages7XREAD STREAMS orders:payments 08# Returns: All 3 messages910# Restart consumer and read again11XREAD STREAMS orders:payments 012# Returns: ALL 3 messages AGAIN!1314# Messages are NOT deleted after reading15XLEN orders:payments
16# Still returns: 3
This means:
Messages are never automatically deleted
Each consumer sees the same messages
Need to track "last read ID" manually
Suitable for event sourcing and audit logs
Different from traditional queue (where consumption removes message)
3.2.4.
XREAD Example via Python Script
1import redis
2import time
34r = redis.Redis(host='localhost', port=6379, decode_responses=True)567defmain():8print("Hello from undestand-command!")91011# Track last ID we processed12# If only new messages are desired, use '$'13# Start from beginning:14last_id ='0'151617defprocess_payment(data):18# Simulate payment processing19 time.sleep(1)# Simulate time taken to process payment20print(f'Payment for order {data["orderID"]} processed.')212223whileTrue:24# Read messages after last_id25 result = r.xread(26 count=10,27 block=5000,# Wait up to 5 seconds28 streams={'orders:payments': last_id}29)3031if result:32# result format: [(stream_name, [(id, data), (id, data), ...])]33 stream_name, messages = result[0]34# result[0] =35# ['orders:payments', [(...), (...), (...), (...), (...), (...), (...)]]36for message_id, data in messages:37print(f'Processing order {data["orderID"]}: ${data["amount"]}')3839# Process payment40 process_payment(data)4142# Update last_id to this message43 last_id = message_id
4445print(f'Processed {len(messages)} messages. Last ID: {last_id}')46else:47print('No new messages, waiting...')484950if __name__ =="__main__":51 main()52
Result:
1Processing order 1001: $598
2Payment for order 1001 processed.
3Processing order 1002: $450
4Payment for order 1002 processed.
5Processing order 1003: $320
6Payment for order 1003 processed.
7Processing order 1003: $320
8Payment for order 1003 processed.
9Processing order 5001: $100
10Payment for order 5001 processed.
11Processing order 5002: $200
12Payment for order 5002 processed.
13Processing order 5003: $300
14Payment for order 5003 processed.
15Processed 7 messages. Last ID: 1772399368367-0
3.2.5.
Limitations of XREAD Without Consumer Groups
Redis Stream has the following limitations:
No automatic consumer coordination
Must manually track last read ID
No built-in ACK mechanism
Cannot distribute messages among multiple consumers
No automatic retry on failure
Each consumer sees all messages (no automatic distribution)
Solution: Stream with Consumer Groups, we will be intrducing it in the next article.
3.3.
Advantages Over BLMOVE
Structured Data (No String Parsing Needed). Redis Streams support structured messages with multiple field-value pairs: