Skip to main content
Message queues enable asynchronous communication between distributed components, improving system performance through decoupling, traffic smoothing, and fault tolerance.

Message Queue Fundamentals

What is a Message Queue?

A message queue is an inter-application communication mechanism where:
  • Producers send messages without waiting for processing
  • Message broker ensures reliable delivery
  • Consumers retrieve and process messages independently
Design Pattern Connection: Message queues implement the Observer pattern - publishers emit events without knowing who will consume them, and subscribers receive events without knowing who published them.

Core Components

Producer

Sends messages to the queue
  • Publishes without waiting
  • No knowledge of consumers
  • Can continue processing immediately

Message Broker

Central message processing system
  • Stores messages reliably
  • Routes to consumers
  • Ensures delivery guarantees

Consumer

Retrieves and processes messages
  • Pulls from queue
  • No knowledge of producers
  • Processes asynchronously

Why Use Message Queues?

Improve system performance by deferring non-critical operations
1

Synchronous problem

Issues: Long response time, user waits for all operations
2

Asynchronous solution

Benefits: Fast response, background processing

Real Example: E-commerce Order

Critical path (synchronous):
  • Validate inventory
  • Process payment
  • Create order record
Background tasks (async via queue):
  • Send SMS notifications
  • Send email receipts
  • Update analytics
  • Log audit trail

Reliability Guarantees

Durability

Messages must not be lost
  • Persist to disk
  • Replicate across nodes
  • Survive broker restarts

At-Least-Once Delivery

Every message consumed at least once
  • Consumer acknowledgment (ACK)
  • Retry on failure
  • Idempotent processing

Ordering

Preserve message order when required
  • Per-partition ordering
  • Single consumer per partition
  • Trade-off with parallelism

Redis as Message Queue

Redis is primarily a cache, but its data structures can implement simple message queues. Three approaches:

Production-Consumption with Redis Lists

Pattern: Producer-Consumer (Point-to-Point)Redis Lists are doubly-linked lists with O(1) insert/delete at both ends - perfect for FIFO queues.

Approach 1: LPUSH + RPOP

# Producer adds to left (head)
LPUSH queue:orders '{"orderId": 123, "amount": 99.99}'

# Consumer removes from right (tail) - FIFO
RPOP queue:orders
Advantages:
  • O(1) time complexity
  • FIFO ordering guaranteed
  • Redis persistence protects server-side data
Critical Issues:
1. Performance Risk - Busy Polling
// BAD: CPU-intensive busy loop
while(true) {
    $result = $redis->rpop("queue");
    if($result) {
        $data = json_decode($result, true);
        // process...
    }
    // Empty queue = wasted CPU cycles!
}
  • Consumers constantly poll even when queue empty
  • Wastes CPU resources
  • Increases Redis load
2. Point-to-Point Only
  • Single consumer gets each message
  • No broadcast/fanout capability
  • First consumer wins (based on speed)
3. Data Safety - Client-Side Risk
RPOP removes from server immediately

Data now only in client memory

If client crashes → Message lost forever!
  • No acknowledgment mechanism
  • Cannot retry failed processing
  • Network issues = data loss
Blocking pop solves the busy-polling problem:
# Consumer blocks until data available or timeout
BRPOP queue:orders 30  # Wait up to 30 seconds
# Returns: 1) "queue:orders" 2) "{message}"

# Wait indefinitely (timeout=0)
BRPOP queue:orders 0
Benefits:
  • No busy-waiting: Client sleeps until message arrives
  • Reduced Redis load: No constant polling
  • Instant processing: Wakes immediately when message published
  • Multiple queues: BRPOP queue1 queue2 queue3 0 (priority order)
How BRPOP works:
  1. If queue has data → Returns immediately
  2. If queue empty → Client connection blocks
  3. When new data arrives → Client instantly awakened
  4. If timeout reached → Returns nil
Exception Handling Required
while True:
    try:
        result = redis.brpop('queue:orders', timeout=30)
        if result:
            process_message(result[1])
    except redis.exceptions.ConnectionError:
        # Server disconnected idle connection
        reconnect()
Redis may close idle connections to save resources. Always handle connection errors!
Still has issues:
  • Client data safety problem remains
  • No ACK mechanism
  • Cannot handle failed processing

Approach 3: LPUSH + LRANGE + RPOP

Strategy: Read first, then consume after processing
# Consumer peeks at message (doesn't remove)
LRANGE queue:orders -1 -1  # Get last element

# Process message...
# If successful:
RPOP queue:orders  # Now remove it
Improves safety:
  • Message stays on server during processing
  • Client crash doesn’t lose message
New problems:
  • Not blocking (back to busy-polling)
  • Duplicate processing risk: If consumer crashes after processing but before RPOP

Approach 4: LPUSH + BRPOPLPUSH + LREM (Most Reliable)

Atomic move to backup queue:
# Atomically move from main queue to processing queue
BRPOPLPUSH queue:orders queue:processing 30

# Consumer processes message...
# On success, remove from processing queue:
LREM queue:processing 1 "{message}"

How It Works

Advantages:
  • Atomic operation: Message never lost between queues
  • Server-side safety: All operations on Redis
  • Crash recovery: Unacknowledged messages stay in processing queue
  • Blocking: No busy-polling
Remaining issue:
Stuck messages: If consumer crashes, messages stuck in queue:processingSolution: Monitoring daemon
# Separate process monitors processing queue
def rescue_stuck_messages():
    while True:
        # Find messages older than timeout
        old_messages = find_old_messages('queue:processing', timeout=300)
        for msg in old_messages:
            # Move back to main queue
            redis.rpoplpush('queue:processing', 'queue:orders')
        time.sleep(60)
This creates a circular queue for automatic retry.

Summary: List-Based Patterns

ApproachBlockingData SafetyDuplicate RiskComplexity
LPUSH + RPOP❌ Server onlyLow
LPUSH + BRPOP❌ Server onlyLow
LPUSH + LRANGE + RPOP⚠️ Better✅ PossibleMedium
LPUSH + BRPOPLPUSH✅ Best✅ Possible*High
*Requires monitoring daemon for stuck message recovery

Redis MQ Pattern Comparison

FeatureList (BRPOP)List (BRPOPLPUSH)Pub/SubStreams
Persistence✅ Yes✅ Yes❌ No✅ Yes
At-Least-Once❌ No⚠️ With monitoring❌ No✅ Yes (ACK)
Multiple Consumers❌ Competing❌ Competing✅ Broadcast✅ Both modes
Consumer Groups❌ No❌ No❌ No✅ Yes
Message Replay❌ No❌ No❌ No✅ Yes
Blocking Read✅ Yes✅ Yes✅ Yes✅ Yes
ComplexityLowMediumLowHigh
Best ForSimple queuesReliable queuesEventsProduction MQ

Design Considerations

Idempotency

Handle duplicate messages gracefully
# Use unique message ID
processed_ids = set()

def process_message(msg_id, data):
    if msg_id in processed_ids:
        return  # Already processed
    
    # Do work...
    processed_ids.add(msg_id)
Store processed IDs in Redis with expiration

Message TTL

Prevent queue bloat
# Add timestamp to messages
message = {
    'data': payload,
    'timestamp': time.time()
}

# Consumer checks age
age = time.time() - msg['timestamp']
if age > MAX_AGE:
    # Discard or move to DLQ
    pass

Error Handling

Implement dead-letter queue
MAX_RETRIES = 3

try:
    process(message)
except Exception as e:
    retries = get_retry_count(msg_id)
    if retries < MAX_RETRIES:
        requeue(message)
    else:
        move_to_dlq(message, error=str(e))

Monitoring

Track queue healthKey metrics:
  • Queue depth (backlog)
  • Processing rate
  • Error rate
  • Consumer lag
  • Message age

Production Message Queue Alternatives

When Redis isn’t enough:For mission-critical message processing with strict reliability requirements, use dedicated message queue systems:

RabbitMQ

Best for: Complex routing, traditional MQ patterns✅ Rich routing (exchanges, bindings) ✅ Strong delivery guarantees ✅ Management UI ✅ Multi-protocol (AMQP, MQTT, STOMP)❌ Lower throughput than Kafka ❌ More operational complexity

Apache Kafka

Best for: High-throughput event streaming✅ Massive scalability (millions msg/s) ✅ Long-term storage ✅ Stream processing (Kafka Streams) ✅ Distributed partitioning❌ Complex setup ❌ Requires ZooKeeper (pre-3.x)

RocketMQ

Best for: E-commerce, financial systems✅ Designed for transactional messages ✅ Scheduled/delayed messages ✅ Alibaba battle-tested ✅ Lower latency than Kafka❌ Smaller ecosystem ❌ Less documentation (English)

Decision Matrix

Good fit for Redis MQ:
  • Already using Redis for caching
  • Simple queue requirements
  • Low-to-medium message volume (less than 10K msg/s)
  • Message loss tolerance (Pub/Sub) or using Streams
  • Need minimal infrastructure
  • Development/testing environments
Example use cases:
  • Email/SMS notification queues
  • Background job processing
  • Cache invalidation
  • Real-time activity feeds

Best Practices Summary

1

Choose the right pattern

  • Simple tasks → List with BRPOP
  • Reliable delivery → List with BRPOPLPUSH or Streams
  • Broadcasting → Pub/Sub (if loss acceptable) or Streams
  • Production critical → Redis Streams or dedicated MQ
2

Design for failure

  • Implement idempotent processing
  • Use acknowledgments (Streams)
  • Set up dead-letter queues
  • Monitor and alert on queue depth
3

Manage resources

  • Trim streams/lists to prevent memory bloat
  • Set message TTLs
  • Monitor Redis memory usage
  • Plan for scaling (add consumers, partition streams)
4

Test failure scenarios

  • Consumer crashes mid-processing
  • Network partitions
  • Redis restarts
  • Message replay after failures

Load Balancing

Distribute synchronous traffic across servers

Service Discovery

Dynamic service location for distributed messaging

Build docs developers (and LLMs) love