Bootcamp backendontwikkeling met Node.js · Les

Acknowledgements, dead-letter-queues en retries

Garandeer aflevering met handmatige acks, verwerk poison messages en implementeer retry- en dead-letterflows.

Les 3 van 413 stappen

Acknowledgements, dead-letter-queues en retries is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met Node.js. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Waarom bevestigingen belangrijk zijn

Standaard moet een consumer, wanneer deze een bericht van RabbitMQ ontvangt, de broker laten weten of het bericht succesvol is verwerkt. Dit signaal heet een bevestiging (ack).

Als je auto-ack inschakelt, verwijdert RabbitMQ een bericht zodra het is afgeleverd. Als je worker vervolgens tijdens de verwerking crasht, is het bericht voorgoed verdwenen. Met handmatige bevestigingen bewaart de broker het bericht totdat je het bevestigt en levert hij het opnieuw af als de consumer uitvalt.

  • auto-ack: snel, maar aflevering van hoogstens één keer (berichten kunnen verloren gaan)
  • handmatige ack: veilig, met aflevering van ten minste één keer

Voor elke taak die echt werk uitvoert (een kaart belasten, een e-mail verzenden, naar een database schrijven) wil je vrijwel altijd handmatige bevestigingen gebruiken.

Consumeren met handmatige bevestigingen

Met de bibliotheek amqplib geef je { noAck: false } door aan channel.consume om handmatige bevestiging in te schakelen. Nadat je handler succesvol is voltooid, roep je channel.ack(msg) aan.

Totdat je het bericht bevestigt, beschouwt RabbitMQ het als niet-bevestigd en levert het opnieuw af als het kanaal of de verbinding wordt gesloten.

const amqp = require('amqplib');

async function start() {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();
  const queue = 'orders';

  await channel.assertQueue(queue, { durable: true });

  channel.consume(queue, async (msg) => {
    if (msg === null) return;
    const order = JSON.parse(msg.content.toString());
    console.log('Processing order', order.id);
    // ... do real work here ...
    channel.ack(msg); // confirm success
  }, { noAck: false });
}

start();

ack, nack en reject

RabbitMQ biedt drie manieren om op een afgeleverd bericht te reageren:

  • channel.ack(msg) — geslaagd, verwijdert het bericht
  • channel.nack(msg, false, requeue) — mislukt; bij requeue=true gaat het bericht terug naar de queue, bij false wordt het verwijderd (of naar een dead-letter-exchange gestuurd)
  • channel.reject(msg, requeue) — hetzelfde als nack, maar alleen voor één bericht

Het tweede argument van nack is allUpTo. Stel dit in op false om alleen op het huidige bericht te reageren; met true geef je elk niet-bevestigd bericht tot en met dit bericht een nack.

Belangrijke beslissing: requeue=true probeert het bericht meteen opnieuw en kan voor probleemberichten tot oneindige lussen leiden. Gebruik liever requeue=false in combinatie met een dead-letterstrategie.

// Failure with no requeue -> message is dropped or dead-lettered
channel.nack(msg, false, false);

// Failure with requeue -> message returns to the front of the queue
channel.nack(msg, false, true);

// reject only ever affects this one message
channel.reject(msg, false);

Prefetch: werk dat wordt uitgevoerd beheren

Zonder limieten stuurt RabbitMQ zoveel mogelijk berichten naar een consumer, die allemaal onbevestigd in het geheugen staan. Dat kan een trage worker overbelasten.

channel.prefetch(n) stelt het maximumaantal onbevestigde berichten in dat een consumer tegelijk mag vasthouden. Met handmatige bevestigingen verwerkt een worker met prefetch(1) één bericht, bevestigt het en ontvangt daarna het volgende — voor een eerlijke verzending met terugdruk.

const channel = await conn.createChannel();
await channel.assertQueue('orders', { durable: true });

// Only one unacked message at a time per consumer
await channel.prefetch(1);

channel.consume('orders', async (msg) => {
  await handle(msg);
  channel.ack(msg);
}, { noAck: false });

Wat is een vergiftigd bericht?

Een vergiftigd bericht is een bericht waarvan de verwerking altijd mislukt — bijvoorbeeld door ongeldige JSON, een verwijzing naar een verwijderd record of een onherstelbare validatiefout. Als je het eenvoudig met nack(msg, false, true) afwijst, wordt het bericht opnieuw in de wachtrij geplaatst, opnieuw afgeleverd, opnieuw verwerkt en opnieuw afgewezen. Zo blijft het oneindig rondgaan en raakt je CPU geblokkeerd.

Je hebt een manier nodig om het na enkele pogingen op te geven en het bericht naar een veilige plek te verplaatsen voor inspectie. Die plek is een Dead-Letter Queue (DLQ).

  • Stop na N pogingen met opnieuw proberen
  • Verplaats het bericht uit de hoofdstroom
  • Bewaar het voor foutopsporing of handmatig opnieuw afspelen

Dead-letter-exchanges

RabbitMQ kan een bericht automatisch naar een andere exchange routeren wanneer het als dead letter wordt behandeld. Een bericht wordt een dead letter wanneer:

  • het wordt afgewezen met requeue=false, of
  • de TTL verloopt, of
  • de wachtrij de maximale lengte overschrijdt

Je configureert dit met de wachtrijargumenten x-dead-letter-exchange (verplicht) en optioneel x-dead-letter-routing-key. Koppel een wachtrij aan die exchange en afgewezen berichten komen daar terecht.

// Main queue routes failures to the 'dlx' exchange
await channel.assertExchange('dlx', 'direct', { durable: true });
await channel.assertQueue('orders.dlq', { durable: true });
await channel.bindQueue('orders.dlq', 'dlx', 'orders.dead');

await channel.assertQueue('orders', {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'dlx',
    'x-dead-letter-routing-key': 'orders.dead'
  }
});

Een fout naar de DLQ sturen

Zodra de wachtrij orders een dead-letter-exchange heeft geconfigureerd, hoef je een bericht alleen maar af te wijzen zonder het opnieuw in de wachtrij te plaatsen.

De broker verzorgt de routering voor je — je publiceert nooit handmatig naar de DLQ. Zo blijft de logica van je consumer overzichtelijk: verwerk het bericht en laat RabbitMQ het bij een onherstelbare fout als dead letter behandelen.

channel.consume('orders', async (msg) => {
  try {
    const order = JSON.parse(msg.content.toString());
    await processOrder(order);
    channel.ack(msg);
  } catch (err) {
    console.error('Unrecoverable failure:', err.message);
    // requeue=false -> RabbitMQ dead-letters to orders.dlq
    channel.nack(msg, false, false);
  }
}, { noAck: false });

Pogingen tellen met headers

Vaak wil je enkele keren opnieuw proberen voordat je opgeeft — een tijdelijke time-out van een database kan bij de tweede poging bijvoorbeeld wel slagen. RabbitMQ heeft geen ingebouwde teller voor nieuwe pogingen, dus houd je de pogingen zelf bij.

Wanneer een bericht als dead letter wordt behandeld, voegt RabbitMQ een array met de header x-death toe die elke dead-letter-gebeurtenis beschrijft, inclusief een count. Je kunt die uitlezen om te bepalen of je opnieuw moet proberen of het bericht naar de uiteindelijke DLQ moet routeren.

function deathCount(msg) {
  const xDeath = msg.properties.headers && msg.properties.headers['x-death'];
  if (!Array.isArray(xDeath) || xDeath.length === 0) return 0;
  // sum counts across dead-letter events for this reason
  return xDeath.reduce((sum, d) => sum + (d.count || 0), 0);
}

const attempts = deathCount(msg);
if (attempts >= 3) {
  channel.nack(msg, false, false); // give up -> parking DLQ
} else {
  // route back for another attempt
}

Vertraagde pogingen met een TTL-wachtrij

Opnieuw in de wachtrij plaatsen probeert het bericht onmiddellijk opnieuw, wat ongeschikt is voor tijdelijke fouten die tijd nodig hebben om te verdwijnen. Een veelgebruikt patroon is een retry-wachtrij met een TTL die berichten als dead letter terugstuurt naar de hoofdwachtrij.

Stroom: fout in de hoofdwachtrij → het bericht gaat naar orders.retry, dat x-message-ttl heeft (bijvoorbeeld 5s) en een dead-letter-exchange die terugwijst naar orders. Nadat de TTL is verlopen, verplaatst RabbitMQ het bericht automatisch terug — zo ontstaat er standaard een vertraging tussen pogingen.

// Retry queue: holds messages for 5s, then dead-letters back to main
await channel.assertQueue('orders.retry', {
  durable: true,
  arguments: {
    'x-message-ttl': 5000,
    'x-dead-letter-exchange': '',          // default exchange
    'x-dead-letter-routing-key': 'orders'  // back to main queue
  }
});

// On a retryable failure, publish into the wait queue instead of nacking
channel.sendToQueue('orders.retry', msg.content, {
  persistent: true,
  headers: msg.properties.headers
});
channel.ack(msg); // ack the original; the copy is now waiting

De retrylogica samenvoegen

Dit is de beslisroute die een robuuste consumer bij een fout volgt:

  • Parseren of valideren mislukt permanent → stuur het bericht onmiddellijk als dead letter naar de parkeer-DLQ
  • Tijdelijke fout en pogingen < maximum → stuur het bericht naar de retry-wachtrij (TTL) voor een vertraagde nieuwe poging
  • Tijdelijke fout maar pogingen ≥ maximum → geef op en stuur het bericht naar de parkeer-DLQ

Zo begrens je de totale hoeveelheid werk, voorkom je eindeloze snelle lussen en bewaar je mislukte berichten voor inspectie of handmatig opnieuw afspelen.

const MAX_RETRIES = 3;

channel.consume('orders', async (msg) => {
  let order;
  try {
    order = JSON.parse(msg.content.toString());
  } catch (e) {
    return channel.nack(msg, false, false); // poison -> parking DLQ
  }

  try {
    await processOrder(order);
    channel.ack(msg);
  } catch (err) {
    const attempts = retryHeader(msg);
    if (attempts >= MAX_RETRIES) {
      channel.nack(msg, false, false);    // exhausted -> parking DLQ
    } else {
      channel.sendToQueue('orders.retry', msg.content, {
        persistent: true,
        headers: { ...msg.properties.headers, 'x-retries': attempts + 1 }
      });
      channel.ack(msg);                    // ack original; retry is queued
    }
  }
}, { noAck: false });

Een uitvoerbare simulatie van opnieuw proberen

Voor het gedrag van RabbitMQ is een broker nodig, maar de beslislogica voor opnieuw proberen is gewone JavaScript die je afzonderlijk kunt begrijpen en testen. Hieronder staat een zelfstandige simulatie van de beslissing rond bevestigen, opnieuw proberen en dead lettering die een order-consumer maakt.

De kern is: slaag en bevestig, probeer opnieuw zolang je onder de limiet blijft en parkeer het bericht in de DLQ zodra de pogingen zijn uitgeput.

const MAX_RETRIES = 3;

function decide(message) {
  // simulate processing: 'good' succeeds, 'bad-json' is poison, else transient
  if (message.kind === 'good') return { action: 'ack' };
  if (message.kind === 'bad-json') return { action: 'dlq', reason: 'poison' };

  const attempts = message.retries || 0;
  if (attempts >= MAX_RETRIES) return { action: 'dlq', reason: 'exhausted' };
  return { action: 'retry', retries: attempts + 1 };
}

const inbox = [
  { id: 1, kind: 'good' },
  { id: 2, kind: 'bad-json' },
  { id: 3, kind: 'transient', retries: 0 },
  { id: 4, kind: 'transient', retries: 3 }
];

for (const msg of inbox) {
  const result = decide(msg);
  console.log('msg ' + msg.id + ' ->', JSON.stringify(result));
}

Korte controle

Je hebt een consumer die handmatige bevestigingen gebruikt. Een bericht bevat ongeldige JSON die nooit kan worden geparseerd. Je wilt voorkomen dat het bericht oneindig blijft rondgaan en het toch bewaren voor latere inspectie.

Samenvatting

Je hebt geleerd hoe je RabbitMQ-levering betrouwbaar en veerkrachtig maakt:

  • Handmatige bevestigingen (noAck: false + channel.ack) zorgen voor levering van minstens één keer, zodat een crash nooit een bericht verloren laat gaan.
  • nack/reject met requeue=false geeft een mislukt bericht door aan een dead-letter-exchange in plaats van het oneindig te laten rondgaan.
  • prefetch(n) zorgt voor terugdruk bij trage workers door het aantal onbevestigde berichten te beperken.
  • Dead-letter-exchanges (x-dead-letter-exchange) routeren fouten automatisch naar een DLQ voor inspectie en opnieuw afspelen.
  • Nieuwe pogingen gebruiken een TTL-wachtrij die berichten als dead letter terugstuurt naar de hoofdwachtrij, met een teller voor pogingen (via x-death of een aangepaste header) om nieuwe pogingen te begrenzen voordat het bericht wordt geparkeerd.

Samen zorgen deze patronen voor begrensde, inzichtelijke en verliesvrije verwerking van berichten.

Gratis beginnen

Leer JavaScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
22
Lessen
92

Veelgestelde vragen

Is de les “Acknowledgements, dead-letter-queues en retries” gratis?

Ja — de volledige tekst van “Acknowledgements, dead-letter-queues en retries” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Bootcamp backendontwikkeling met Node.js wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Wat leer ik in “Acknowledgements, dead-letter-queues en retries”?

Garandeer aflevering met handmatige acks, verwerk poison messages en implementeer retry- en dead-letterflows. Je oefent met Bootcamp backendontwikkeling met Node.js door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met Node.js te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met Node.js op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “Acknowledgements, dead-letter-queues en retries”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met Node.js?

Ja. Elke les over Bootcamp backendontwikkeling met Node.js bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Producers, consumers en het AMQP-model
  2. Exchangetypen: direct, topic, fanout en headers
  3. Acknowledgements, dead-letter-queues en retries
  4. Work queues, prefetch en concurrerende consumers
← Terug naar Bootcamp backendontwikkeling met Node.js