Design async task queues: when to queue vs do work inline, producers/consumers, dead-letter queues, retry with exponential backoff + jitter, idempotent consumers, and at-least-once vs at-most-once delivery semantics. Triggers on 'should I queue this', 'add a job queue', 'dead letter queue', 'retry backoff', 'idempotent consumer', 'task queue design'.
./install.sh message-queuesname: "message_queues" description: "Design async task queues: when to queue vs do work inline, producers/consumers, dead-letter queues, retry with exponential backoff + jitter, idempotent consumers, and at-least-once vs at-most-once delivery semantics. Triggers on 'should I queue this', 'add a job queue', 'dead letter queue', 'retry backoff', 'idempotent consumer', 'task queue design'." metadata: { "includeInPrompt": true }
> Differs from webhook-handling because: this is the ASYNC task-queue side — producers/consumers, DLQs, retry backoff, delivery semantics. webhook-handling is the synchronous receiving side: verifying and retrying inbound HTTP callbacks. Complements it; does not overlap.
Turn slow, bursty, or failure-prone work into reliable background jobs. Use this when a task asks for a job queue, delayed/scheduled work, cross-service messaging, or when a synchronous request handler is doing work it shouldn't.
Queue when any of these are true:
Do NOT queue when: the caller needs the result in this request; the work is trivially fast (<~100ms, no I/O); strict global ordering matters (queues guarantee ordering only per partition/queue, not across workers — check your broker's docs); or adding infrastructure would exceed the reliability the task actually needs.
Retry formula: delay = base * 2^attempt + jitter, where jitter is a random fraction of the base. The 2^attempt spreads retries exponentially so a flapping dependency isn't hammered; jitter (add random.uniform(0, base)) stops all workers from retrying in lockstep (thundering herd). Cap the delay (e.g. max 5–15 minutes) and cap attempts (3–5 is typical).
Because at-least-once redelivers, the same message can arrive twice. Three patterns, in preference order:
Save this as queue_demo.py, then run python3 queue_demo.py. It demonstrates producer/consumer, transient retry with backoff + jitter, DLQ routing for a poison message after 3 attempts, and a duplicate delivery skipped by the idempotency check.
python
"""queue_demo.py — async task queue demo (Python stdlib only).
Producer/consumer, retry with exponential backoff + jitter, dead-letter
queue routing after N failures, and an idempotent consumer (dedupe set).
Run: python3 queue_demo.py"""
import queue, random, threading, time
MAX_ATTEMPTS, BASE_DELAY = 3, 0.1
work_q, processed_ids, DLQ = queue.Queue(), set(), []
transient_seen, lock = set(), threading.Lock()
random.seed(42) # deterministic jitter for the demo
def backoff(attempt): # delay = base * 2^attempt + jitter
return BASE_DELAY * 2 ** attempt + random.uniform(0, 0.05)
def do_work(tid):
if tid == 3: # poison message: always fails
raise RuntimeError("poison message")
with lock: # task 5: one transient failure, then succeeds
if tid == 5 and tid not in transient_seen:
transient_seen.add(tid)
raise TimeoutError("transient failure")
def producer(tasks):
for t in tasks:
work_q.put({"id": t, "attempts": 0})
print(f"[producer] enqueued task {t}")
def consumer():
while True:
task = work_q.get()
if task is None: # shutdown sentinel — still ack it, or join() hangs
work_q.task_done()
break
tid, attempts = task["id"], task["attempts"]
with lock:
if tid in processed_ids:
print(f"[consumer] task {tid}: duplicate delivery — skipped (idempotent)")
work_q.task_done()
continue
try:
do_work(tid)
except Exception as e:
attempts += 1
if attempts >= MAX_ATTEMPTS:
DLQ.append({"id": tid, "error": str(e)})
print(f"[consumer] task {tid}: failed {attempts}x ({e}) -> DEAD-LETTER QUEUE")
else:
d = backoff(attempts)
print(f"[consumer] task {tid}: attempt {attempts} failed ({e}); retry in {d:.2f}s")
time.sleep(d)
work_q.put({"id": tid, "attempts": attempts})
else:
with lock:
processed_ids.add(tid)
print(f"[consumer] task {tid}: done (ack)")
work_q.task_done() # ack ONLY after work completes or is parked
t = threading.Thread(target=consumer)
t.start()
producer([1, 2, 3, 4, 5, 6])
producer([2]) # simulate at-least-once redelivery of an already-done task
work_q.join() # drain all work first — retries requeue during this
work_q.put(None) # sentinel only after the queue is truly empty
t.join()
print(f"\nprocessed IDs: {sorted(processed_ids)}")
print(f"DLQ: {[m['id'] for m in DLQ]}")
Verified output (Python 3, stdlib only, run locally):
[producer] enqueued task 1
[producer] enqueued task 2
[producer] enqueued task 3
[producer] enqueued task 4
[producer] enqueued task 5
[producer] enqueued task 6
[producer] enqueued task 2
[consumer] task 1: done (ack)
[consumer] task 2: done (ack)
[consumer] task 3: attempt 1 failed (poison message); retry in 0.23s
[consumer] task 4: done (ack)
[consumer] task 5: attempt 1 failed (transient failure); retry in 0.20s
[consumer] task 6: done (ack)
[consumer] task 2: duplicate delivery — skipped (idempotent)
[consumer] task 3: attempt 2 failed (poison message); retry in 0.41s
[consumer] task 5: done (ack)
[consumer] task 3: failed 3x (poison message) -> DEAD-LETTER QUEUE
processed IDs: [1, 2, 4, 5, 6]
DLQ: [3]
The demo above is the mental model, not production. When you need durability across restarts, multiple workers, or cross-service messaging, reach for a real broker — RabbitMQ, Apache Kafka, Redis Streams, or AWS SQS/SNS. All of them implement some version of: queues/topics, consumers, ack/redelivery (visibility timeout), retries, and DLQs. Broker APIs and CLIs vary by version and deployment, so check your broker's docs for the exact commands rather than trusting recalled syntax.
When this skill fires, deliver:
publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.