Jour 4 Day 4 · jeudi 30 juillet 2026 Thursday 30 July 2026 Backend Intermédiaire

RabbitMQ & les files de messages RabbitMQ & message queues

Découplage, lissage de charge, garanties de livraison : ce qu'un broker de messages change dans une architecture — et les questions ack, DLQ et idempotence qui reviennent en boucle en entretien backend. Decoupling, load smoothing, delivery guarantees: what a message broker changes in an architecture — and the ack, DLQ and idempotence questions that come up over and over in backend interviews.

L’essentiel

Une message queue insère un intermédiaire durable entre un producteur et un consommateur : au lieu d’appeler le service B en synchrone (et d’échouer avec lui), le service A publie un message dans un broker et continue sa vie. Trois bénéfices : découplage (A ignore qui consomme, B peut être down sans casser A), lissage de charge (un pic de trafic s’empile dans la queue au lieu d’écrouler B), résilience (le message persiste jusqu’à son traitement, avec retry natif).

RabbitMQ est le broker open source le plus répandu, implémentation de référence du protocole AMQP 0-9-1. En entretien, on attend trois choses : le modèle exchange/queue/binding, la mécanique des acks, et la discussion at-least-once / idempotence.

Comment ça marche

Le modèle AMQP a une subtilité que les débutants ratent : un producteur ne publie jamais directement dans une queue. Il publie dans un exchange, avec une routing key ; l’exchange route le message vers zéro, une ou plusieurs queues selon ses bindings (les règles de liaison exchange → queue).

             routing key
Producteur ─────────────▶ Exchange
                           │ bindings
             ┌─────────────┼─────────────┐
             ▼             ▼             ▼
          Queue A       Queue B       Queue C
             │             │             │
             ▼             ▼             ▼
        Consumer 1    Consumer 2    Consumer 3
                    (ack / nack)

Les quatre types d’exchange :

TypeRoutageCas typique
directrouting key exactepayment.failed → la queue liée avec cette clé
fanouttoutes les queues liées, clé ignoréepub/sub pur, invalidation de cache
topicmotif sur la clé segmentée par des points : * = exactement un mot, # = zéro ou pluslogs.*.error matche logs.api.error, pas logs.api.db.error
headerssur les en-têtes du messagerarement utilisé

Côté consommateur : le broker pousse les messages, et le consumer les acquitte (ack) une fois le traitement terminé. Si le consumer meurt avant l’ack (crash, connexion coupée), le broker redélivre le message — flag redelivered positionné — à un autre consumer. nack/reject refusent un message, avec ou sans remise en queue. Le prefetch (QoS) borne le nombre de messages non acquittés par consumer : c’est lui qui assure une répartition équitable au lieu de tout déverser sur le premier connecté.

Publier et consommer, en Python (pika) :

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
ch = conn.channel()
ch.queue_declare(queue="tasks", durable=True)  # queue durable

# --- Producteur ---
ch.basic_publish(
    exchange="",                    # exchange par défaut (direct)
    routing_key="tasks",            # ici : le nom de la queue
    body=b"resize image 42",
    properties=pika.BasicProperties(delivery_mode=2),  # message persistent
)

# --- Consommateur ---
def handle(ch, method, props, body):
    process(body)                                   # le vrai travail d'abord
    ch.basic_ack(delivery_tag=method.delivery_tag)  # l'ack APRÈS, jamais avant

ch.basic_qos(prefetch_count=10)  # borne les messages non acquittés
ch.basic_consume(queue="tasks", on_message_callback=handle)
ch.start_consuming()

⚠️ L’ack oublié — consommer en ack manuel sans jamais appeler basic_ack : les messages s’entassent en unacked, le prefetch se remplit, le consumer ne reçoit plus rien — et tout est redélivré d’un coup à la reconnexion. Symptôme classique : « la queue a l’air vide mais rien n’avance » — regarder la colonne unacked du management UI.

💡 Deux niveaux, sinon rien — la durabilité se déclare queue durable et message persistent (delivery_mode=2) : l’un sans l’autre ne survit pas à un redémarrage du broker. Côté producteur, les publisher confirms donnent l’accusé de réception du broker.

Dead letter queue (DLQ) : une queue peut déclarer un dead-letter exchange (DLX) ; y sont routés les messages rejetés sans requeue, expirés (TTL) ou en dépassement de longueur maximale. Indispensable en production : un message empoisonné (qui fait crasher le consumer) part en DLQ après N tentatives au lieu de tourner en boucle, et on peut l’inspecter puis le rejouer.

Concepts clés à maîtriser

  • at-most-once vs at-least-once : ack automatique (auto-ack) = at-most-once — le message est considéré livré dès l’envoi, un crash du consumer le perd. Ack manuel après traitement = at-least-once — rien n’est perdu, mais un crash entre le traitement et l’ack provoque une redélivrance en double. L’« exactly-once » de bout en bout n’existe pas dans un système distribué sans coopération applicative.
  • Idempotence des consumers : conséquence directe du at-least-once — le consumer doit tolérer les doublons. Techniques : déduplication par message id (insert avec contrainte unique → le doublon échoue proprement), ou opérations naturellement idempotentes (upsert, SET status = 'paid' plutôt que balance += x).
  • Patterns : work queue (une queue, N consumers en compétition — chaque message traité une fois, scaling horizontal du traitement), pub/sub (exchange fanout ou topic, une queue par service consommateur — chacun reçoit sa copie de l’événement), RPC sur messaging (queue de réponse + correlation_id — possible, mais réintroduit un couplage synchrone : à utiliser avec parcimonie).
  • RabbitMQ vs Kafka : Rabbit est une queue — broker intelligent qui route finement, message supprimé après ack, parfait pour la distribution de tâches. Kafka est un log distribué append-only — les messages restent, chaque consumer avance son offset, ce qui permet le replay et plusieurs lectures indépendantes du même flux ; débit massif grâce aux partitions. Tâches, routage riche, latence faible → Rabbit ; event streaming, replay, très haut débit → Kafka.
  • Ordering : garanti FIFO au sein d’une queue… pour un seul consumer. Avec des consumers en compétition ou des redélivrances, l’ordre de traitement n’est plus garanti — à ne jamais promettre en entretien.

En entretien

🎤 En entretien — le fil rouge qui impressionne : ack manuel → at-least-once → doublons possibles → consumer idempotent. Dérouler cette chaîne de cause à effet sans qu’on vous la demande, c’est exactement ce qu’on attend d’un profil backend.

« Pourquoi mettre une queue entre deux services plutôt qu’un appel HTTP ? » — Découplage temporel (B peut être down, le message attend), lissage des pics (la queue absorbe, B consomme à son rythme), retry natif via la redélivrance, et fan-out vers plusieurs consommateurs sans toucher au producteur. Contreparties à citer spontanément : latence de bout en bout, cohérence éventuelle, et une brique d’infra de plus à opérer et monitorer.

« Direct, fanout, topic : tu utilises quoi, quand ? » — Direct pour du routage exact (chaque type de tâche vers sa queue), fanout pour diffuser un événement à tout le monde (invalidation de cache), topic pour du routage par motifs (order.* pour tous les événements commande, #.error pour toutes les erreurs, quelle que soit la source).

« Que se passe-t-il si le consumer crashe au milieu du traitement ? » — Avec ack manuel : le broker détecte la fermeture du canal, remet le message en queue (flag redelivered) et un autre consumer le reprend — rien n’est perdu, mais le traitement a pu être partiellement appliqué, d’où l’exigence d’idempotence. Avec auto-ack : le message est perdu, point.

« At-least-once : quel problème ça pose et comment tu le gères ? » — Des doublons. On rend le consumer idempotent : déduplication par identifiant de message stocké avec contrainte unique, ou opérations idempotentes par nature. On ne « règle » pas les doublons côté broker — c’est une responsabilité applicative.

« RabbitMQ ou Kafka pour ce use case ? » — La question ouverte classique. Grille de lecture : besoin de rejouer l’historique, très haut débit, plusieurs équipes lisant le même flux d’événements → Kafka. Distribution de tâches, routage fin, TTL, priorités, latence faible sur volumes modérés → RabbitMQ. Bonus : dire que les deux coexistent souvent dans une même stack.

Pièges & idées reçues

  • Auto-ack en production : séduisant (plus simple, plus rapide), mais tout crash consumer = messages perdus silencieusement. L’ack manuel après traitement est le réglage par défaut raisonnable.
  • Queue durable ≠ messages persistants : il faut les deux — queue déclarée durable et messages publiés en persistent — sinon un redémarrage du broker efface tout.
  • Queue qui enfle sans limite : si les consumers sont durablement plus lents que les producteurs, la queue grossit jusqu’à saturer la mémoire puis le disque du broker. Monitorer la profondeur des queues, poser TTL et max-length avec un DLX.
  • Croire à l’exactly-once magique : aucune option du broker ne l’offre de bout en bout ; la vraie réponse est at-least-once + idempotence applicative.
  • Message empoisonné sans DLQ : rejeté avec requeue, il revient immédiatement en tête de queue, refait crasher le consumer, revient… boucle infinie qui bloque tout. DLQ + compteur de tentatives obligatoires.
  • Faire du RPC partout par-dessus la queue : on cumule la latence du broker et le couplage du synchrone. Si A a besoin de la réponse de B immédiatement, un appel HTTP/gRPC direct est souvent plus honnête.

Pour aller plus loin

The essentials

A message queue inserts a durable intermediary between a producer and a consumer: instead of calling service B synchronously (and failing along with it), service A publishes a message to a broker and moves on. Three benefits: decoupling (A doesn’t know who consumes, B can be down without breaking A), load smoothing (a traffic spike piles up in the queue instead of crushing B), resilience (the message persists until processed, with native retry).

RabbitMQ is the most widespread open source broker, the reference implementation of the AMQP 0-9-1 protocol. In an interview, three things are expected: the exchange/queue/binding model, the ack mechanics, and the at-least-once / idempotence discussion.

How it works

The AMQP model has a subtlety beginners miss: a producer never publishes directly to a queue. It publishes to an exchange, with a routing key; the exchange routes the message to zero, one or several queues according to its bindings (the exchange → queue link rules).

            routing key
Producer ───────────────▶ Exchange
                           │ bindings
             ┌─────────────┼─────────────┐
             ▼             ▼             ▼
          Queue A       Queue B       Queue C
             │             │             │
             ▼             ▼             ▼
        Consumer 1    Consumer 2    Consumer 3
                    (ack / nack)

The four exchange types:

TypeRoutingTypical case
directexact routing keypayment.failed → the queue bound with that key
fanoutevery bound queue, key ignoredpure pub/sub, cache invalidation
topicpattern on the dot-separated key: * = exactly one word, # = zero or morelogs.*.error matches logs.api.error, not logs.api.db.error
headerson message headersrarely used

On the consumer side: the broker pushes messages, and the consumer acknowledges (ack) each one once processing is done. If the consumer dies before the ack (crash, dropped connection), the broker redelivers the message — redelivered flag set — to another consumer. nack/reject refuse a message, with or without requeueing. Prefetch (QoS) caps the number of unacknowledged messages per consumer: it’s what ensures fair dispatch instead of dumping everything on the first connected consumer.

Publish and consume, in Python (pika):

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
ch = conn.channel()
ch.queue_declare(queue="tasks", durable=True)  # durable queue

# --- Producer ---
ch.basic_publish(
    exchange="",                    # default exchange (direct)
    routing_key="tasks",            # here: the queue name
    body=b"resize image 42",
    properties=pika.BasicProperties(delivery_mode=2),  # persistent message
)

# --- Consumer ---
def handle(ch, method, props, body):
    process(body)                                   # the real work first
    ch.basic_ack(delivery_tag=method.delivery_tag)  # ack AFTER, never before

ch.basic_qos(prefetch_count=10)  # caps unacknowledged messages
ch.basic_consume(queue="tasks", on_message_callback=handle)
ch.start_consuming()

⚠️ The forgotten ack — consuming with manual ack but never calling basic_ack: messages pile up as unacked, the prefetch fills up, the consumer receives nothing more — and everything is redelivered at once on reconnection. Classic symptom: “the queue looks empty but nothing moves” — check the unacked column in the management UI.

💡 Both levels or nothing — durability is declared durable queue and persistent message (delivery_mode=2): one without the other does not survive a broker restart. On the producer side, publisher confirms provide the broker’s acknowledgment of receipt.

Dead letter queue (DLQ): a queue can declare a dead-letter exchange (DLX); messages rejected without requeue, expired (TTL) or overflowing the max length are routed there. Essential in production: a poison message (one that crashes the consumer) goes to the DLQ after N attempts instead of looping forever, and you can inspect it then replay it.

Key concepts to master

  • at-most-once vs at-least-once: automatic ack (auto-ack) = at-most-once — the message is considered delivered as soon as it’s sent, a consumer crash loses it. Manual ack after processing = at-least-once — nothing is lost, but a crash between processing and ack causes a duplicate redelivery. End-to-end “exactly-once” does not exist in a distributed system without application-level cooperation.
  • Consumer idempotence: the direct consequence of at-least-once — the consumer must tolerate duplicates. Techniques: deduplication by message id (insert with a unique constraint → the duplicate fails cleanly), or naturally idempotent operations (upsert, SET status = 'paid' rather than balance += x).
  • Patterns: work queue (one queue, N competing consumers — each message processed once, horizontal scaling of processing), pub/sub (fanout or topic exchange, one queue per consuming service — each gets its own copy of the event), RPC over messaging (reply queue + correlation_id — possible, but it reintroduces synchronous coupling: use sparingly).
  • RabbitMQ vs Kafka: Rabbit is a queue — a smart broker with fine-grained routing, messages deleted after ack, perfect for task distribution. Kafka is a distributed append-only log — messages stay, each consumer advances its offset, which enables replay and multiple independent reads of the same stream; massive throughput thanks to partitions. Tasks, rich routing, low latency → Rabbit; event streaming, replay, very high throughput → Kafka.
  • Ordering: FIFO guaranteed within a queue… for a single consumer. With competing consumers or redeliveries, processing order is no longer guaranteed — never promise it in an interview.

In an interview

🎤 In an interview — the thread that impresses: manual ack → at-least-once → possible duplicates → idempotent consumer. Unrolling this cause-and-effect chain unprompted is exactly what’s expected from a backend profile.

“Why put a queue between two services rather than an HTTP call?” — Temporal decoupling (B can be down, the message waits), spike smoothing (the queue absorbs, B consumes at its own pace), native retry through redelivery, and fan-out to several consumers without touching the producer. Trade-offs to mention unprompted: end-to-end latency, eventual consistency, and one more piece of infrastructure to operate and monitor.

“Direct, fanout, topic: which do you use, when?” — Direct for exact routing (each task type to its queue), fanout to broadcast an event to everyone (cache invalidation), topic for pattern routing (order.* for all order events, #.error for all errors, whatever the source).

“What happens if the consumer crashes mid-processing?” — With manual ack: the broker detects the channel closing, requeues the message (redelivered flag) and another consumer picks it up — nothing is lost, but processing may have been partially applied, hence the idempotence requirement. With auto-ack: the message is lost, full stop.

“At-least-once: what problem does it cause and how do you handle it?” — Duplicates. You make the consumer idempotent: deduplication by message id stored with a unique constraint, or naturally idempotent operations. You don’t “fix” duplicates on the broker side — it’s an application responsibility.

“RabbitMQ or Kafka for this use case?” — The classic open-ended question. Decision grid: need to replay history, very high throughput, several teams reading the same event stream → Kafka. Task distribution, fine-grained routing, TTL, priorities, low latency on moderate volumes → RabbitMQ. Bonus: mention that both often coexist in the same stack.

Pitfalls & misconceptions

  • Auto-ack in production: tempting (simpler, faster), but any consumer crash = messages silently lost. Manual ack after processing is the sane default.
  • Durable queue ≠ persistent messages: you need both — queue declared durable and messages published as persistent — otherwise a broker restart wipes everything.
  • Unbounded queue growth: if consumers are durably slower than producers, the queue grows until it saturates the broker’s memory then disk. Monitor queue depth, set TTL and max-length with a DLX.
  • Believing in magic exactly-once: no broker option provides it end to end; the real answer is at-least-once + application-level idempotence.
  • Poison message without a DLQ: rejected with requeue, it comes right back to the head of the queue, crashes the consumer again, comes back… an infinite loop that blocks everything. DLQ + retry counter are mandatory.
  • Doing RPC everywhere over the queue: you stack the broker’s latency on top of synchronous coupling. If A needs B’s answer right now, a direct HTTP/gRPC call is often more honest.

Going further

S'entraîner sur ce sujet → Practice this topic →