Bootcamp backendontwikkeling met Node.js · Les

Timeouts, retries en exponentiële back-off met jitter

Begrens elke uitgaande aanroep en probeer tijdelijke fouten opnieuw zonder de belasting van afhankelijkheden te versterken.

Les 1 van 413 stappen

Timeouts, retries en exponentiële back-off met jitter is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 1 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 je elke uitgaande aanroep begrenst

In een Node.js-backend kan elke aanroep die je proces verlaat (HTTP-verzoek, databasequery, publicatie naar een berichtenbroker) voor altijd blijven hangen. Een trage afhankelijkheid vertraagt niet alleen één verzoek, maar houdt ook een event-loop-tik en een socket bezet en houdt een plek in de verbindingspool vast.

  • Onbegrensde latentie stapelt zich op: één vastgelopen aanroep naar een downstreamservice verandert in duizenden vastgelopen inkomende verzoeken.
  • Uitputting van bronnen: sockets, bestandsdescriptors en poolverbindingen blijven bezet terwijl je wacht.
  • Geen SLA zonder deadline: je kunt geen p99-latentie beloven als een aanroep geen bovengrens heeft.

De eerste regel voor veerkracht in productie: doe nooit een uitgaande aanroep zonder time-out. Opnieuw proberen en terughoudendheid bouwen op die begrenzing voort.

Time-outs met AbortSignal.timeout

Moderne Node.js (18+) bevat standaard AbortSignal.timeout(ms), waarmee je een signaal maakt dat automatisch wordt afgebroken na de deadline. De globale fetch accepteert een signal, dus je begrenst een HTTP-aanroep in één regel.

  • Wanneer de time-out afgaat, wordt de promise afgewezen met een AbortError (err.name === 'AbortError').
  • Het signaal werkt zonder verdere actie: je hoeft clearTimeout niet handmatig aan te roepen.
  • Maak altijd onderscheid tussen een afgebroken time-out en andere netwerkfouten, zodat je correct logt en opnieuw probeert.
async function fetchWithTimeout(url, ms) {
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(ms) });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError' || err.name === 'TimeoutError') {
      throw new Error('Request timed out after ' + ms + 'ms');
    }
    throw err;
  }
}

// Demo against a hung promise (no real network)
function hang(signal) {
  return new Promise((_, reject) => {
    signal.addEventListener('abort', () => reject(signal.reason));
  });
}

(async () => {
  try {
    await hang(AbortSignal.timeout(50));
  } catch (e) {
    console.log('Aborted:', e.name);
  }
})();

Verbindings-, lees- en totale time-outs verschillen

"Time-out" is niet één getal. Een robuuste client onderscheidt verschillende deadlines:

  • Verbindingstime-out: hoe lang je wacht op de TCP/TLS-handshake.
  • Lees-time-out / time-out bij inactiviteit van de socket: hoe lang je tussen bytes wacht nadat de verbinding tot stand is gebracht.
  • Totale time-out / time-out voor de hele bewerking: een harde grens voor de volledige bewerking, inclusief nieuwe pogingen.

Een veelgemaakte fout is alleen een lees-time-out instellen. Een afhankelijkheid die de verbinding accepteert maar nooit de eerste byte verstuurt, kan nog steeds tot aan de limiet voor socketinactiviteit blokkeren. Het totale budget is de grens die de aanroeper daadwerkelijk merkt. Beperk daarom altijd de volledige reeks nieuwe pogingen, niet alleen elke afzonderlijke poging.

Vuistregel: perAttemptTimeout x maxAttempts moet onder je totale verzoekbudget blijven, anders overschrijd je je eigen SLA tijdens het opnieuw proberen.

Probeer alleen tijdelijke, idempotente fouten opnieuw

Opnieuw proberen is gevaarlijk als je het blind toepast. Classificeer de fout voordat je het opnieuw probeert:

  • Opnieuw proberen (tijdelijk): verbroken verbinding, DNS-hapering, time-out, HTTP 502/503/504 en 429 (met inachtneming van Retry-After).
  • NIET opnieuw proberen: 400 (ongeldig verzoek), 401/403 (authenticatie), 404, 422. Opnieuw proberen verhoogt hierbij alleen de belasting en kan nooit slagen.
  • Idempotentie is belangrijk: een GET of PUT kun je veilig herhalen; een niet-idempotente POST (een kaart belasten) kan twee keer worden uitgevoerd. Gebruik een idempotentiesleutel voordat je schrijfbewerkingen opnieuw probeert.
function isRetryable(err) {
  // Network-level errors thrown by Node
  const transientCodes = new Set([
    'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN'
  ]);
  if (err.code && transientCodes.has(err.code)) return true;
  if (err.name === 'AbortError' || err.name === 'TimeoutError') return true;
  // HTTP status carried on the error
  if (err.status && [429, 502, 503, 504].includes(err.status)) return true;
  return false;
}

console.log(isRetryable({ code: 'ECONNRESET' }));   // true
console.log(isRetryable({ status: 503 }));           // true
console.log(isRetryable({ status: 404 }));           // false
console.log(isRetryable(new Error('bad json')));     // false

Opnieuw proberen met een vaste vertraging is niet genoeg

Bij de eenvoudige aanpak wacht je tussen pogingen steeds even lang:

  • Als een afhankelijkheid kortstondig overbelast is, proberen al je clients met hetzelfde vaste interval opnieuw en belasten ze die afhankelijkheid weer tegelijk.
  • Zo ontstaat een storm van nieuwe pogingen: juist het opnieuw proberen houdt de afhankelijkheid buiten werking.
  • Een constant korte vertraging verspilt pogingen als de storing seconden duurt, terwijl een constant lange vertraging tijd verspilt bij korte haperingen.

De oplossing is om na elke fout langer te wachten (exponentiële terughoudendheid), zodat de druk op de afhankelijkheid na verloop van tijd afneemt, en om willekeurigheid toe te voegen (jitter), zodat clients niet synchroniseren. In de volgende scènes bouwen we dit op.

Exponentiële terughoudendheid

Bij exponentiële terughoudendheid vermenigvuldig je de vertraging na elke mislukte poging, doorgaans met een basis van 2:

  • delay = base * 2^attempt, bijvoorbeeld met basis 100ms: 100, 200, 400, 800ms.
  • Beperk de vertraging altijd met een maxDelay, zodat die niet oploopt tot minuten.
  • Door deze bovengrens verandert zuivere exponentiële groei in "begrensde exponentiële terughoudendheid", wat je vrijwel altijd wilt.

Het idee: een snelle eerste nieuwe poging vangt een eenmalige hapering op, terwijl latere pogingen sterk vertragen om een worstelende afhankelijkheid tijd te geven om te herstellen.

function backoffDelay(attempt, base = 100, maxDelay = 2000) {
  const exp = base * 2 ** attempt;
  return Math.min(exp, maxDelay);
}

for (let attempt = 0; attempt < 6; attempt++) {
  console.log('attempt', attempt, '->', backoffDelay(attempt) + 'ms');
}
// 0->100, 1->200, 2->400, 3->800, 4->1600, 5->2000 (capped)

Het probleem van de stormloop

Ook zuivere exponentiële terughoudendheid heeft een tekortkoming: als 1.000 clients op hetzelfde moment falen (omdat een gedeelde afhankelijkheid kort haperde), berekenen ze allemaal dezelfde vertraging en proberen ze op hetzelfde toekomstige moment opnieuw.

  • De afhankelijkheid herstelt en krijgt vervolgens 1.000 gelijktijdige nieuwe pogingen te verwerken, waardoor ze opnieuw uitvalt.
  • Deze gesynchroniseerde golf is de stormloop.
  • De vertraging begrenzen helpt niet; het synchroniseert de stormloop alleen bij de bovengrens.

De oplossing is jitter: voeg willekeurigheid toe aan de vertraging van elke client, zodat de nieuwe pogingen over een venster worden verspreid in plaats van op dezelfde tik te vallen. Jitter is geen optionele verfijning, maar het onderdeel dat de afhankelijkheid daadwerkelijk beschermt.

Volledige jitter

De door AWS aanbevolen strategie is volledige jitter: bereken de begrensde exponentiële bovengrens en kies vervolgens een gelijkmatig willekeurige vertraging tussen 0 en die bovengrens.

  • cap = min(maxDelay, base * 2^attempt)
  • delay = random(0, cap)

Zo worden nieuwe pogingen gelijkmatig over het volledige venster verspreid en botsingen tot een minimum beperkt. Vergeleken met "gelijke jitter" (de helft vast + de helft willekeurig) leidt volledige jitter over het algemeen tot de minste gelijktijdige belasting en het kleinste totale aantal aanroepen bij hoge belasting. Het kleine nadeel is dat een afzonderlijke nieuwe poging al heel vroeg kan plaatsvinden. Dat is prima, want het doel is de stormloop te desynchroniseren.

function fullJitterDelay(attempt, base = 100, maxDelay = 2000) {
  const cap = Math.min(maxDelay, base * 2 ** attempt);
  return Math.floor(Math.random() * cap);
}

// Show how 5 "clients" spread out on the same attempt
for (let client = 0; client < 5; client++) {
  console.log('client', client, 'attempt 3 delay:', fullJitterDelay(3) + 'ms');
}
// Each client gets a different value in [0, 800)

Alles samenvoegen: retryWithBackoff

Combineer nu begrenzing, classificatie, begrensde exponentiële terughoudendheid en volledige jitter in één herbruikbare helper. Belangrijke ontwerppunten:

  • Elke poging wordt afzonderlijk begrensd door een time-out.
  • Alleen fouten waarvoor opnieuw proberen zinvol is starten een volgende poging; alle andere fouten worden onmiddellijk doorgegeven.
  • Geef na de laatste poging de fout opnieuw door, zodat de aanroeper snel kan falen.
  • Een totale deadline (hier niet getoond) moet in productie nog steeds de volledige lus omvatten.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

function fullJitter(attempt, base = 100, maxDelay = 2000) {
  const cap = Math.min(maxDelay, base * 2 ** attempt);
  return Math.floor(Math.random() * cap);
}

async function retryWithBackoff(fn, { retries = 4, isRetryable } = {}) {
  let lastErr;
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn(attempt);
    } catch (err) {
      lastErr = err;
      if (attempt === retries || !isRetryable(err)) throw err;
      const delay = fullJitter(attempt);
      console.log('attempt', attempt, 'failed, waiting', delay + 'ms');
      await sleep(delay);
    }
  }
  throw lastErr;
}

// Simulate a flaky call that succeeds on the 3rd try
let calls = 0;
retryWithBackoff(
  async () => {
    calls++;
    if (calls < 3) { const e = new Error('flaky'); e.status = 503; throw e; }
    return 'ok after ' + calls + ' calls';
  },
  { retries: 4, isRetryable: (e) => e.status === 503 }
).then(console.log);

Respecteer Retry-After en begrens het totale budget

Twee verfijningen voor productie maken het verschil tussen een beleefde client en een loadversterker:

  • Respecteer Retry-After: bij 429 of 503 kan de server precies aangeven hoe lang je moet wachten. Geef altijd de voorkeur aan die waarde boven je berekende back-off: de dependency vraagt daarmee om ruimte.
  • Leg een algemene deadline op: houd een budget bij (bijvoorbeeld 3s). Controleer vóór het wachten of now + delay de deadline zou overschrijden. Stop dan met opnieuw proberen en geef meteen een fout terug, in plaats van je SLA te overschrijden.

Door deze combinatie blijft elke poging begrensd, blijft het totaal begrensd en kan de dependency haar eigen herstel sturen.

function nextDelay(err, attempt, base = 100, maxDelay = 5000) {
  const header = err.retryAfterSeconds; // parsed from Retry-After
  if (typeof header === 'number') return header * 1000;
  const cap = Math.min(maxDelay, base * 2 ** attempt);
  return Math.floor(Math.random() * cap);
}

const deadline = Date.now() + 3000; // 3s total budget
let attempt = 0;
const err = { status: 429, retryAfterSeconds: 1 };
const delay = nextDelay(err, attempt);

if (Date.now() + delay > deadline) {
  console.log('Budget exhausted, fail fast');
} else {
  console.log('Honoring Retry-After, sleeping', delay + 'ms');
}

Herhaalde pogingen hebben een circuitonderbreker nodig

Herhaalde pogingen behandelen tijdelijke fouten. Ze zijn het verkeerde hulpmiddel bij een aanhoudende storing: als een dependency volledig uitvalt, vermenigvuldigt elk verzoek dat vier keer opnieuw wordt geprobeerd je uitgaande belasting met een factor 5, precies op het slechtst mogelijke moment.

  • Plaats een circuitonderbreker boven de helper voor herhaalde pogingen. Wanneer het aantal fouten een drempel overschrijdt, opent de onderbreker en worden aanroepen onmiddellijk kortgesloten (direct afgebroken), in plaats van opnieuw te worden geprobeerd.
  • Na een afkoelperiode gaat de onderbreker naar half-open, laat hij één proefaanroep door en sluit hij weer bij succes.
  • Volgorde van de lagen: onderbreker -> opnieuw proberen -> time-out. De time-out begrenst elke poging, opnieuw proberen handelt korte storingen af en de onderbreker stopt de schade tijdens echte storingen.

Dit is de volledige veerkrachtlaag voor uitgaande aanroepen in een Node.js-backend.

Korte controle

Je beheert een Node.js-service waarvan de externe betalingsprovider tijdens een implementatie kort 503 retourneert. Duizenden exemplaren van je service proberen het allemaal opnieuw. Welke ene wijziging voorkomt het meest rechtstreeks dat je herhaalde pogingen de provider opnieuw overbelasten zodra deze herstelt?

Samenvatting

Je hebt geleerd hoe je uitgaande aanroepen kunt begrenzen en opnieuw kunt proberen zonder de belasting te versterken:

  • Begrens alles: doe geen uitgaande aanroep zonder time-out; onderscheid verbindings-, lees- en totale deadlines met AbortSignal.timeout.
  • Classificeer vóór je opnieuw probeert: probeer alleen tijdelijke, idempotente fouten opnieuw (time-outs, ECONNRESET, 429/502/503/504); probeer 400/401/404/422 nooit opnieuw.
  • Exponentiële back-off met bovengrens: verleng de vertraging na elke fout, maar begrens deze met maxDelay.
  • Volledige jitter: kies een willekeurige vertraging in [0, cap) om de stormloop van gelijktijdige verzoeken te doorbreken. Dit onderdeel beschermt de dependency.
  • Respecteer Retry-After en een totaalbudget: laat de server het herstel sturen en geef snel een fout terug voordat je je SLA overschrijdt.
  • Voeg een circuitonderbreker toe: onderbreker -> opnieuw proberen -> time-out behandelt respectievelijk storingen, korte onderbrekingen en trage aanroepen.
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 “Timeouts, retries en exponentiële back-off met jitter” gratis?

Ja — de volledige tekst van “Timeouts, retries en exponentiële back-off met jitter” 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 “Timeouts, retries en exponentiële back-off met jitter”?

Begrens elke uitgaande aanroep en probeer tijdelijke fouten opnieuw zonder de belasting van afhankelijkheden te versterken. 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 1 van 4.

Hoe lang duurt de les “Timeouts, retries en exponentiële back-off met jitter”?

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. Timeouts, retries en exponentiële back-off met jitter
  2. Het circuit-breakerpatroon en bulkhead-isolatie
  3. Graceful shutdown en het leegmaken van actieve requests
  4. Healthchecks, readiness probes en load shedding
← Terug naar Bootcamp backendontwikkeling met Node.js