System Design

System Design Adventure-16

Message Queues: Building Systems That Don't Break

Posted by Afsal on 16 Oct 2026

Hi Pythonistas!

Your system is working.
Users signing up.
Orders being placed.
Everything running fine.
Then you add a new feature.

When user signs up:
1. Save user to database
2. Send welcome email
3. Send SMS verification
4. Create default settings
5. Notify analytics
6. Set up storage bucket
Six things. One request.

def signup(request):
    user = db.save_user(request.data)        # 50ms
    email.send_welcome(user.email)            # 300ms
    sms.send_verification(user.phone)         # 200ms
    db.create_default_settings(user.id)       # 50ms
    analytics.track_signup(user.id)           # 150ms
    s3.create_user_bucket(user.id)            # 400ms
    return {'success': True}                  # total: 1150ms

User waits 1.15 seconds to sign up.
And if the email service is down?
Signup fails.
User can't create an account.
Because of an email.
There's a better way.

Synchronous vs Asynchronous

Synchronous:

Do one thing.
Wait for it to finish.
Do the next.
Task 1 → wait → Task 2 → wait → Task 3 → done
Simple. But slow.
One failure breaks everything.

Asynchronous:

Start a task.
Don't wait.
Move on.
Task runs in background.
Task 1 → return response
Task 2, 3, 4 run in background
Fast response.
Tasks run independently.
One failure doesn't break others.

What Is a Message Queue?

A buffer between:
the part that creates work (producer).
and the part that does work (consumer).
Producer → [Queue] → Consumer
Producer puts message in queue.
Consumer picks it up and processes it.
They never talk directly.
Completely decoupled.

The Restaurant Analogy

Customer (producer) → gives order to waiter.
Waiter puts order on kitchen counter (queue).
Chef (consumer) picks up and cooks.
Customer doesn't wait at the counter.
Sits down.
Gets food when ready.
Kitchen processes at their own pace.
One chef sick → others keep working.
Kitchen overwhelmed → orders wait in queue.
Nobody loses their order.

How It Solves the Signup Problem

def signup(request):
    # save user (must be synchronous)
    user = db.save_user(request.data)    # 50ms

    # everything else → queue
    queue.publish('user.signup', {
        'user_id': user.id,
        'email': user.email,
        'phone': user.phone
    })

    return {'success': True}             # total: 55ms

Response in 55ms instead of 1150ms.

20x faster.
Background worker handles the rest:

@queue.subscribe('user.signup')
def handle_signup(message):
    email.send_welcome(message['email'])
    sms.send_verification(message['phone'])
    db.create_default_settings(message['user_id'])
    analytics.track_signup(message['user_id'])
    s3.create_user_bucket(message['user_id'])

Email service down?
Message stays in queue.
Retried automatically.
User already signed up successfully.

Core Concepts

Producer: Creates messages, Your app server. Your API.

Consumer (Worker):
Picks up and processes messages. Separate process.

Message:
Unit of work. Usually JSON.

Queue:  Buffer between producer and consumer.

Persistent: survives restarts.

Acknowledgement (ACK): Consumer tells queue: "processed successfully." Queue removes message.

No ACK → queue retries.

def process_message(message):
    try:
        send_email(message)
        message.ack()    # done ✅
    except Exception:
        message.nack()   # failed, retry ❌

What Happens Without ACK
Consumer picks up message.
Starts processing.
Consumer crashes mid-way.
Without ACK system:
message gone.
work never completed.
With ACK system:
queue notices: no ACK received.
message put back.
another consumer picks it up.
work eventually completes.
This is why ACK exists.

Can Critical Items Go to Queue?

Important question. Answer: it depends.

What Makes Something Critical:
Must happen before response     → synchronous
User needs it immediately       → synchronous
Financial reservation           → synchronous
Data consistency required now   → synchronous

Can happen later                → queue
User doesn't need it now        → queue
Eventual consistency ok         → queue

Real example order placement

def place_order(request):
    with db.transaction():
        # SYNCHRONOUS — user needs these now
        validate_payment(request.payment)    # must work now
        reserve_inventory(request.items)     # must work now
        charge_payment(request.payment)      # must work now
        order = create_order(request)        # must work now

        # write to outbox (same transaction)
        write_to_outbox([
            {'event': 'order.confirmation_email'},
            {'event': 'order.warehouse_notify'},
            {'event': 'order.analytics'},
        ])

    # fast response to user
    return {'order_id': order.id, 'status': 'confirmed'}

    # background → email, warehouse, analytics

Payment = synchronous.
Notifications = queue.

The Outbox Pattern

Critical async work needs special handling.
What if queue crashes after payment?
Message lost.
User charged. No order created.
Disaster.
Solution Outbox Pattern:
Write to database AND outbox table in same transaction.

with db.transaction():
    order = db.save_order(data)

    # same transaction
    db.save_outbox_message({
        'event': 'order.created',
        'order_id': order.id
    })
    # if transaction fails → both rolled back
    # if transaction succeeds → both saved


Separate process publishes outbox messages to queue:

while True:
    messages = db.get_pending_outbox_messages()
    for message in messages:
        queue.publish(message.event, message.data)
        db.delete_outbox_message(message.id)

Database and queue always in sync.
Message never lost.
Even if queue temporarily down.

The Decision Framework

Does user need result immediately?
YES → synchronous
NO  → queue

If this fails, does whole operation fail?
YES → synchronous
NO  → queue with retry

Is financial reservation involved?
YES → synchronous + DB transaction
NO  → queue

Can money or inventory be duplicated?
YES (cannot) → synchronous + outbox pattern
NO           → queue is fine

Simple rule:

Synchronous:
→ payment, auth, inventory reservation
→ anything user needs RIGHT NOW

Queue:
→ emails, SMS, push notifications
→ analytics and logging
→ search indexing
→ generating reports
→ cache invalidation
→ webhooks

Popular Message Queues

RabbitMQ

  • Traditional message broker.
  • Complex routing support.
  • Good for: task queues, fan-out.

AWS SQS

  • Managed. No infrastructure.
  • Scales automatically.
  • Good for: AWS systems, serverless.

Redis (as queue)

Simple. Already in your stack.
Good for: low volume, simple tasks.

Kafka

  • Not just a queue.
  • Event streaming platform.
  • Messages stored permanently.
  • Multiple consumers read same messages independently.
  • High throughput.

More on Kafka soon.

Patterns

Task Queue

Work distributed across workers.
Each message processed by ONE worker.
Producer → [Queue] → Worker1
                  → Worker2
                  → Worker3
Used for: emails, image processing, reports.

Pub/Sub

One producer. Multiple consumers.
Each gets a copy.
Producer → [Topic] → Consumer A (email)
                  → Consumer B (analytics)
                  → Consumer C (notifications)
Used for: event broadcasting, microservices.

Dead Letter Queue (DLQ)

Message fails repeatedly.
Move to DLQ instead of losing it.
[Main Queue] → Consumer (fails 3 times)
                    ↓
             [Dead Letter Queue]
                    ↓
             investigate / fix / retry

Never lose a failed message.

Fan-out

One event → multiple queues → multiple consumers.
Order placed
    ↓
SNS Topic
    ↓          ↓            ↓
Email Queue  SMS Queue  Analytics Queue
    ↓          ↓            ↓
Email Worker SMS Worker Analytics Worker
Each service independent.
One slow → doesn't affect others.

At Least Once vs Exactly Once

At Least Once:

Message delivered at least once.
Might be delivered multiple times.
Network hiccup:
Consumer processes message.
ACK not received.
Queue retries.
Consumer processes again.
User gets two welcome emails.
Fix - idempotent consumer:

Exactly Once:

Message delivered exactly once.
Hard to achieve.
Kafka supports with transactions.
Most systems: at-least-once + idempotent consumers.

Message Queues in Real Systems

E-commerce:

Order placed → queue → inventory update
                    → payment processing
                    → shipping notification
                    → analytics

Social media:

Post created → queue → notify followers
                    → update feed
                    → index for search
                    → content moderation

Ride sharing:

Trip completed → queue → process payment
                      → update driver earnings
                      → send rating request
                      → update analytics

Benefits

Decoupling:

Without queue: Email service down → signup fails

With queue: Email service down → message waits → signup succeeds → email sent when recovered

Buffering:

10,000 signups in 1 minute. Email service handles 100/minute.

Without queue: email service overwhelmed
With queue:    messages wait, processed at own pace

Scalability:

Queue growing too fast? Add more consumers. They automatically pick up messages.

Retry:

Processing fails? Message goes back to queue. Retried automatically.

Mental Model

Producer       → creates messages
Consumer       → processes messages
Queue          → buffer between them
ACK            → confirms successful processing
NACK           → signals failure, triggers retry
DLQ            → failed messages after max retries
Decoupling     → producer and consumer independent
Buffering      → absorbs traffic spikes
Outbox pattern → write DB + queue atomically
At least once  → might process twice
Exactly once   → complex, Kafka transactions
Idempotency    → safe to process twice
RabbitMQ       → task queues, routing
Kafka          → event streaming, replay
SQS            → managed, AWS ecosystem
Pub/Sub        → one message, many consumers
Fan-out        → one event, multiple queues

Synchronous    → payment, auth, inventory
Queue          → everything else

What's Coming Next

Now your system handles failures gracefully.
But what about protecting it from too much traffic?
100,000 requests per second hitting your API.
Your server can handle 1,000.
What do you do?
Rate Limiting.
How to protect your system
from being overwhelmed.
By accident or by attack

← Previous Post

Recent posts