Distributed Queue Design — Kafka, RabbitMQ & SQS Guide
In this tutorial, you'll learn about Distributed Queue Design. We cover key concepts, practical examples, and best practices.
A distributed queue decouples producers and consumers by buffering messages between them, providing fault-tolerant, scalable, and asynchronous communication — a cornerstone of event-driven microservices architecture.
Why Distributed Queue Design Matters
Synchronous communication couples services — if one is slow or down, callers wait or fail. Queues decouple them. A producer sends a message and continues immediately. The consumer processes when it can. This pattern handles traffic spikes, service outages, and gradual scale-up. Kafka processes 2+ trillion messages daily at LinkedIn. RabbitMQ handles 1M+ messages per second at Alibaba. SQS processes billions of requests per day across AWS. DodaZIP's compression pipeline uses RabbitMQ to distribute ZIP jobs across worker nodes, scaling from 10 to 100 workers during peak hours.
Plain-Language Explanation
Imagine a busy restaurant kitchen. Waiters (producers) place order tickets on a spindle (the queue). Chefs (consumers) pick up tickets from the spindle, cook the food, and discard the ticket. If a chef is slow, tickets queue up on the spindle. If a new chef starts their shift, they just start picking tickets. The spindle doesn't care how many waiters or chefs there are. It's a buffer that decouples order-taking from cooking.
graph LR
P1[Producer 1] -->|Messages| Queue{Distributed Queue}
P2[Producer 2] -->|Messages| Queue
P3[Producer N] -->|Messages| Queue
Queue -->|Partition 0| C1[Consumer 1
Group A]
Queue -->|Partition 1| C2[Consumer 2
Group A]
Queue -->|Partition 2| C3[Consumer 3
Group A]
Queue -->|Fan-out| C4[Consumer
Group B]
Queue -->|Dead Letter| DLQ[DLQ
Failed Messages]
style Queue fill:#e67e22,color:#fff
style C1 fill:#3498db,color:#fff
style C2 fill:#3498db,color:#fff
style C3 fill:#3498db,color:#fff
style DLQ fill:#e74c3c,color:#fff
Kafka Producer and Consumer
from kafka import KafkaProducer, KafkaConsumer
import json
producer = KafkaProducer(
bootstrap_servers=['localhost:9092'],
value_serializer=lambda v: json.dumps(v).encode(),
acks='all',
retries=3,
)
producer.send('order-events', {'order_id': 'ORD-123', 'amount': 99.99})
producer.flush()
consumer = KafkaConsumer(
'order-events',
bootstrap_servers=['localhost:9092'],
group_id='order-processor',
auto_offset_reset='earliest',
enable_auto_commit=True,
)
for message in consumer:
event = json.loads(message.value)
print(f"Processing order {event['order_id']}: ${event['amount']}")
# process and auto-commit offset
Expected output:
Processing order ORD-123: $99.99
RabbitMQ Exchange and Queue
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='orders', exchange_type='topic')
channel.queue_declare(queue='order_payments', durable=True)
channel.queue_bind(exchange='orders', queue='order_payments', routing_key='order.paid')
channel.basic_publish(
exchange='orders',
routing_key='order.paid',
body='{"order_id": "ORD-123", "amount": 99.99}',
properties=pika.BasicProperties(delivery_mode=2),
)
def callback(ch, method, properties, body):
print(f"Received: {body}")
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(queue='order_payments', on_message_callback=callback, auto_ack=False)
SQS with Boto3
import boto3, json
sqs = boto3.client('sqs', region_name='us-east-1')
queue_url = 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
sqs.send_message(
QueueUrl=queue_url,
MessageBody=json.dumps({'order_id': 'ORD-123', 'amount': 99.99}),
MessageGroupId='orders',
MessageDeduplicationId='ORD-123-1719244800',
)
response = sqs.receive_message(
QueueUrl=queue_url,
MaxNumberOfMessages=10,
WaitTimeSeconds=20,
)
if 'Messages' in response:
for msg in response['Messages']:
print(f"Processing: {msg['Body']}")
sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=msg['ReceiptHandle'])
Kafka Partition Assignment
Custom partition strategy for ordering guarantees:
class OrderPartitioner:
def partition(self, key: bytes, partitions: list[int]) -> int:
order_id = key.decode()
user_id = order_id.split("-")[0]
return hash(user_id) % len(partitions)
producer = KafkaProducer(
bootstrap_servers=['localhost:9092'],
partitioner=OrderPartitioner(),
key_serializer=str.encode,
)
Common Mistakes
No dead-letter queue: Poison messages (always failing) block the queue. Configure a DLQ for messages that exceed the retry limit.
Ignoring message ordering: Kafka preserves order within a partition. SQS FIFO queues preserve order. RabbitMQ doesn't guarantee order with multiple consumers.
At-most-once vs at-least-once confusion: Auto-commit before processing can lose messages on crash. Manual ack after processing may reprocess. Choose based on tolerance:
- At-most-once: accept message loss, no duplicates
- At-least-once: no loss, possible duplicates
- Exactly-once: use idempotent consumers
Infinite retries: A failing message retries forever, blocking other messages. Use a retry limit (3-5) then send to DLQ.
No monitoring: Queue depth growth indicates consumers are falling behind. Alert on queue depth exceeding a threshold (1000+ messages).
Practice Questions
Kafka vs RabbitMQ: when to use which? Kafka for high-throughput event streaming (100K+ msgs/sec), long-term retention, and replay. RabbitMQ for complex routing, low-latency task queues, and RPC-style patterns.
How does Kafka achieve high throughput? Sequential disk I/O, zero-copy data transfer, batching, partitioning, and compression. Kafka writes to disk sequentially at speeds comparable to network.
What is a consumer group? Multiple consumers that jointly consume a topic. Each partition is assigned to one consumer in the group, enabling parallel processing while preserving partition order.
How does SQS guarantee at-least-once delivery? Messages are stored redundantly across multiple AZs. A consumer receives a message, it becomes invisible for the visibility timeout. If the consumer doesn't delete it within that timeout, it reappears for redelivery.
What is backpressure and how do you handle it? When producers send faster than consumers can process. Solutions: scale consumers, use a larger queue, implement rate limiting, or use a circuit breaker to reject excess messages.
Mini Project
Build a simple task queue with Redis:
import redis, uuid, time, threading
class TaskQueue:
def __init__(self, queue_name: str = "tasks"):
self.r = redis.Redis(decode_responses=True)
self.queue_name = queue_name
def enqueue(self, task_type: str, payload: dict) -> str:
task_id = str(uuid.uuid4())
task = {"id": task_id, "type": task_type, "payload": payload, "created_at": time.time()}
self.r.rpush(self.queue_name, json.dumps(task))
return task_id
def dequeue(self, timeout: int = 5) -> dict:
result = self.r.blpop(self.queue_name, timeout=timeout)
if result:
return json.loads(result[1])
return None
queue = TaskQueue()
def worker():
while True:
task = queue.dequeue(timeout=1)
if task:
print(f"Processing {task['type']}: {task['payload']}")
time.sleep(0.5)
t = threading.Thread(target=worker, daemon=True)
t.start()
queue.enqueue("send_email", {"to": "user@example.com", "subject": "Welcome"})
queue.enqueue("generate_report", {"report_id": "R-001"})
queue.enqueue("process_payment", {"order_id": "O-123", "amount": 49.99})
time.sleep(3)
Cross-References
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro