Skip to content

Distributed Queue Design — Kafka, RabbitMQ & SQS Guide

DodaTech Updated 2026-06-24 4 min read

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

  1. No dead-letter queue: Poison messages (always failing) block the queue. Configure a DLQ for messages that exceed the retry limit.

  2. Ignoring message ordering: Kafka preserves order within a partition. SQS FIFO queues preserve order. RabbitMQ doesn't guarantee order with multiple consumers.

  3. 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
  4. Infinite retries: A failing message retries forever, blocking other messages. Use a retry limit (3-5) then send to DLQ.

  5. No monitoring: Queue depth growth indicates consumers are falling behind. Alert on queue depth exceeding a threshold (1000+ messages).

Practice Questions

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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