Day 6: Move slow work off the request path

Scale ShopStream: System Design in 7 Builds (Python) ~7 min read 45 min hands-on

Move receipt emails onto a Redis Streams queue (checkout 302 β†’ 0.9 ms) with retries, a dead-letter queue and idempotent workers that survive a crash.

After an order is placed, ShopStream emails a receipt. The email provider takes 300 ms and sometimes fails. Sent inside the request, that's 300 ms added to every checkout, and a failed email either fails the order or is silently lost. In this lesson you move receipts onto a queue β€” a Redis Stream β€” so checkout returns in under a millisecond. Then you try hard to break the workers: random provider failures, a message that can never succeed, and a worker that crashes at the worst possible moment. Every receipt still goes out exactly once.

The problem

The rule of thumb: do in the request only what the response depends on. The shopper needs to know the order is placed. They don't need to wait for an email to be accepted by a third party. Work like that β€” emails, thumbnails, search indexing, webhooks, analytics β€” belongs on a queue, processed by workers at their own pace.

A queue raises its own questions, and they're the ones interviewers ask: What if a worker crashes mid-job? What if a job fails forever? Can a customer get two emails?

What you build

  • receipts.py β€” send_receipt(order) simulates the provider: sleeps SEND_MS (300 ms) and fails with probability FAIL_RATE. Orders from user poison always fail. enqueue(order) adds a job with XADD receipts * ….

  • app.py β€” after the order is placed: with RECEIPTS=inline, send the email in the request; with RECEIPTS=queue (the default), XADD and return. GET /queue shows waiting jobs, pending jobs and dead letters.

  • worker.py β€” a consumer in the group mailers.

  • checkout.py β€” times 40 checkouts. queue_demo.py β€” the failure test.

Why Redis Streams

A Redis list with LPUSH/BRPOP is a queue, but once a worker pops a job, Redis forgets it. If the worker crashes, the job is gone. A stream is an append-only log with consumer groups: each job is delivered to one consumer in the group, and stays in the group's pending entries list (PEL) until that consumer acknowledges it with XACK. Crashed? The job is still pending and another worker can claim it. Kafka works the same way with offsets, at much larger scale β€” that's the next course.

The worker loop

Three ideas are in that loop:

Read the full lesson free

This lesson is one of the first 3 in its course, free with an account β€” the full article, implementation guide and scripts. No card needed.

Already a member? Sign in