Crux Read real RabbitMQ binding configs and routing snippets, predict which queues receive a copy, and pick the fix a senior engineer would make first.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
Routing bugs are read in the binding table and the publish call, not in a stack trace — there usually isn’t one. Read each config and snippet, then choose the behaviour or fix a senior engineer would land first.
Goal
Practise the loop you run in every routing incident: read the exchange type and bindings, predict which queues get a copy, and reach for the correct fix before guessing at the producer.
With routing key order.created.eu published to the topic exchange events, which queues receive a copy?
Heads-up Topic bindings are not ranked or exclusive — there is no most-specific-wins. Every binding whose pattern matches independently delivers a copy, so all three queues receive the message.
Heads-up # matches zero or more words, so order.# matches order.created.eu fully. Q_audit receives a copy too.
Heads-up A routing key can satisfy any number of bindings at once; that fan-out to multiple queues is exactly what topic exchanges are for.
Snippet 2 — the prefetch config
channel.basic_qos(prefetch_count=1000) # one channel, many consumerschannel.basic_consume(queue="jobs", on_message_callback=handle)# handle() does slow CPU-bound work, ~200ms per message
Quiz
Completed
Several workers consume jobs with this config and work is unevenly distributed while broker memory climbs. What is the defect and the fix?
Heads-up Polling with basic_get is slower and still does not fix distribution; the push model is fine. The defect is the oversized prefetch reserving a large unacked batch on one consumer.
Heads-up Durability controls whether messages survive a broker restart, not how in-flight messages are distributed across consumers. The skew and memory growth come from the high prefetch.
Heads-up A high prefetch helps only when consumers are fast and uniform; with 200ms CPU-bound work it causes head-of-line starvation and unbounded unacked memory. Low prefetch is the senior default.
Snippet 3 — the dead-letter config
declare queue "work" with arguments: x-dead-letter-exchange: "dlx" x-message-ttl: 30000 # 30sexchange "dlx" (type: fanout) -> queue "work.dead"consumer on "work": on failure -> channel.basic_nack(requeue=True)
Quiz
Completed
A poison message keeps looping on queue work and never reaches work.dead despite the dead-letter config. Why?
Heads-up Any exchange type can be a dead-letter target; fanout to a single quarantine queue is fine. The reason nothing arrives is that requeue=True never lets the message become a dead letter.
Heads-up TTL would eventually dead-letter an untouched message at 30s, but here the consumer immediately requeues it, resetting it onto the active queue before TTL fires. The requeue flag is the real defect.
Heads-up Dead-lettering works on classic and quorum queues alike. The message loops because requeue=True keeps re-delivering it instead of rejecting it to the dlx.
Snippet 4 — the default exchange
# no exchange declared; publish with empty exchange namechannel.basic_publish(exchange="", routing_key="reports", body=payload)# a queue named "reports" exists but is ALSO bound to a topic exchange "events"
Quiz
Completed
Where does this message go, given the empty exchange name and routing key reports?
Heads-up The empty exchange name selects the default (nameless) direct exchange, not events. The message never touches events on this publish.
Heads-up The empty string is a valid, always-present exchange: the default direct exchange. It is the one case where a routing key behaves like a queue name.
Heads-up Empty name is the default direct exchange, not a fanout. It delivers only to the queue whose name equals the routing key — here, reports.
Recap
Routing is read in the binding table, not the stack trace: topic patterns match independently and every match yields a copy (* is exactly one word, # is zero or more); a too-high prefetch reserves a large unacked batch on one consumer and starves the rest; dead-lettering only fires on reject/expire, so nack-with-requeue loops a poison message forever; and the empty-string exchange is the default direct exchange where the routing key is the queue name. Read the type and bindings first, predict the copies, then fix.