Skip to content

Lesson 3 of 9 · 20m

Queues: Decoupling the Request Path

Why slow work doesn't belong in the request path, how at-least-once delivery really works, and the idempotency pattern every consumer needs.

The single most underrated scaling move: move work out of the request path. A signup that takes 2s because it sends email, resizes the avatar, and indexes the user is a signup that fails at 2s — the same work queued finishes in 50ms and the user sees a response immediately.

The delivery guarantee is a lie

No queue guarantees exactly-once. Every queue is at-least-once (the message might be delivered twice — retries, timeouts, consumer crashes) or at-most-once (drop on failure — fine for metrics, wrong for payments). Design consumers to be idempotent: processing the same message twice produces the same result.

def handle(job):
    if db.exists(job.id):      # idempotency check — already processed
        return
    process(job)
    db.mark_done(job.id)

The check-and-mark must be atomic (same transaction as the work) or the idempotency claim is fiction.

Choosing a queue

  • SQS / Pub/Sub — managed, pay-per-use, right for most async work.
  • RabbitMQ — when you need routing flexibility (topic exchanges, priority queues).
  • Postgres (SKIP LOCKED) — for small systems, one less infrastructure piece; fine until it isn't (~10k jobs/sec).
  • Kafka — a log, not a queue; right for event streaming, wrong for task processing.

The poison message problem

A message that crashes the consumer gets retried forever (in at-least-once) or dropped (at-most-once). Fix: a dead-letter queue after N retries — failed messages land there for inspection, the consumer moves on. No DLQ means a single bad message takes down your entire job pipeline.

::exercise(idempotent-consumer)

Field notes

  • Queue depth is the metric — a queue that grows faster than consumers drain it is an outage in slow motion.
  • Timeout on visibility (visibility_timeout) is what makes at-least-once work — a message invisible to other consumers while being processed.
  • The queue consumer is the trust boundary — validate the payload again; "the producer sent it" isn't a type guarantee.