Herstart een WebSocket-server waar tienduizend clients op aangesloten zijn en er gebeurt iets onaangenaams. Elke client merkt de verbroken verbinding op ongeveer hetzelfde moment op, elke client wacht hetzelfde vaste aantal seconden, en elke client belt op dezelfde tik weer aan. De server komt op en wordt onmiddellijk geraakt door tienduizend gelijktijdige TLS-handshakes. Hij valt om, of hij knijpt af, en de clients proberen het opnieuw, weer allemaal in de pas. Dit is de thundering herd, en een vast reconnect-interval garandeert die.
sgcWebSockets 2026.7 pakt de twee plekken aan waar clients een server bestoken die al in de problemen zit. De WatchDog kan nu opnieuw verbinden met exponentiële backoff plus jitter in plaats van met een vast interval, en de HTTP-client kan een mislukt verzoek zelf opnieuw versturen, met respect voor de Retry-After-hint van de server, met dezelfde backoff-berekening. Beide zijn optioneel en staan standaard uit, dus bestaande toepassingen gedragen zich precies zoals voorheen totdat u ze inschakelt.
Het probleem met een vast reconnect-interval
De WatchDog is altijd de standaardmanier geweest om een client verbonden te houden. U zet Enabled, u stelt een Interval in seconden in, en wanneer de verbinding wegvalt probeert de component het elke Interval seconden opnieuw tot hij terug is.
Dat is prima voor één client op een wankele Wi-Fi-verbinding. Het is het verkeerde gedrag voor een vloot. Met een vast interval is elke client in de vloot een metronoom die op hetzelfde tempo tikt, en een serverherstart synchroniseert ze allemaal: ze zijn allemaal op hetzelfde moment hun verbinding kwijtgeraakt, dus ze verbinden allemaal op hetzelfde moment opnieuw, tot in het oneindige. Erger nog, het interval past zich niet aan. Als de server tien minuten uit de lucht is, doet een client met Interval = 1 zeshonderd zinloze verbindingspogingen, en iedereen doet precies hetzelfde.
Wat u wilt is het tegenovergestelde. Probeer het de eerste keer snel opnieuw, want de meeste verbroken verbindingen zijn tijdelijk en een snelle reconnect is onzichtbaar voor de gebruiker. Bouw daarna af, zodat een server die echt uit de lucht is niet wordt gebeukt. En spreid de clients, zodat ze niet allemaal tegelijk aankomen.
Exponentiële backoff op de WatchDog
Het options-object van de WatchDog heeft vier properties gekregen. Het type staat in sgcTCP_Classes:
type
TsgcWatchDogBackoff = (wdbFixed, wdbExponential);
Op elke WebSocket-component zijn de opties bereikbaar via Client.WatchDog, en dit zijn de backoff-instellingen:
uses
sgcWebSocket, sgcTCP_Classes;
begin
sgcWebSocketClient1.WatchDog.Enabled := True;
sgcWebSocketClient1.WatchDog.Attempts := 0; // 0 = keep trying forever
sgcWebSocketClient1.WatchDog.Interval := 1; // seconds, the first delay
sgcWebSocketClient1.WatchDog.Backoff := wdbExponential;
sgcWebSocketClient1.WatchDog.BackoffMultiplier := 2.0; // double it every attempt
sgcWebSocketClient1.WatchDog.MaxInterval := 60; // seconds, the ceiling
sgcWebSocketClient1.WatchDog.Jitter := 0.2; // up to 20% random spread
end;
De berekening is bewust saai. De eerste reconnect-poging wacht Interval. Elke volgende poging vermenigvuldigt de vorige vertraging met BackoffMultiplier, en het resultaat wordt afgekapt op MaxInterval. Met de waarden hierboven lopen de vertragingen 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, 60s enzovoort. De teller wordt gereset zodra de client weer verbonden is, dus een korte netwerkhapering kost u één seconde, geen minuut.
Interval en MaxInterval worden allebei uitgedrukt in seconden, zoals bij de WatchDog altijd al het geval was. Een MaxInterval van 0 betekent geen plafond, en dat is de standaard. Als u exponentiële backoff inschakelt, stel dan een plafond in, anders blijft de vertraging verdubbelen tot ze in uren wordt gemeten.
Het belangrijke detail voor bestaande gebruikers: Backoff staat standaard op wdbFixed. Als u upgradet en niets wijzigt, verbindt uw client opnieuw op hetzelfde vaste interval als altijd. De berekening van de vertraging wordt alleen gebruikt wanneer u overschakelt naar wdbExponential of een Jitter instelt, en het is dezelfde helper die de HTTP-retry gebruikt, sgcNextBackoffMs in sgcBase_Helpers.
Jitter, en waarom het op schaal uitmaakt
Exponentiële backoff alleen breekt de kudde niet. Het zorgt er alleen voor dat de kudde minder vaak arriveert. Tienduizend clients die allemaal 1s wachten, dan 2s, dan 4s, zijn nog steeds tienduizend clients die samen arriveren, alleen in steeds grotere golven.
Jitter is wat ze daadwerkelijk uit elkaar trekt. Het is een fractie van de berekende vertraging, willekeurig toegepast per client en per poging. Met Jitter = 0.2 wordt een nominale vertraging van 8 seconden een willekeurige waarde in een band van 20% rond 8 seconden, dus client A wordt wakker op 7,1s, client B op 8,6s, client C op 8,0s. De vloot arriveert als een vlek in plaats van als een piek, en de accept queue van de server kan het bijbenen.
// A conservative fleet profile: fast first retry, hard ceiling, generous spread.
sgcWebSocketClient1.WatchDog.Enabled := True;
sgcWebSocketClient1.WatchDog.Interval := 2;
sgcWebSocketClient1.WatchDog.Backoff := wdbExponential;
sgcWebSocketClient1.WatchDog.BackoffMultiplier := 1.5; // gentler than doubling
sgcWebSocketClient1.WatchDog.MaxInterval := 120;
sgcWebSocketClient1.WatchDog.Jitter := 0.5; // spread over half the delay
Jitter staat los van de backoff-modus. U kunt Backoff op wdbFixed laten staan en alleen Jitter instellen, wat u het klassieke vaste interval geeft met gedesynchroniseerde clients. Dat is op zichzelf vaak al genoeg, en het is de kleinst mogelijke wijziging aan een bestaande toepassing.
Een HTTP-verzoek automatisch opnieuw versturen
De tweede helft van de functie zit aan de HTTP-kant. Eén HTTP-verzoek dat mislukt met een 503 omdat de server opnieuw wordt uitgerold, of met een 429 omdat u over een rate limit ging, is niet echt een fout. Het is een verzoek dat zo meteen opnieuw verstuurd zou moeten worden. Die lus met de hand schrijven is vervelend en iedereen schrijft hem net iets verkeerd.
TsgcIdHTTP heeft nu een RetryOptions-object, gedeclareerd in sgcHTTP_Client als TsgcIdHTTPRetry_Options. Schakel het in en het verzoek probeert het zelf opnieuw:
uses
sgcHTTP_Client;
var
oHTTP: TsgcIdHTTP;
begin
oHTTP := TsgcIdHTTP.Create(nil);
try
oHTTP.RetryOptions.Enabled := True; // default False
oHTTP.RetryOptions.MaxRetries := 3;
oHTTP.RetryOptions.InitialInterval := 500; // milliseconds
oHTTP.RetryOptions.MaxInterval := 30000; // milliseconds
oHTTP.RetryOptions.Multiplier := 2.0;
oHTTP.RetryOptions.Jitter := 0.2;
oHTTP.RetryOptions.StatusCodes := '429,502,503,504';
ShowMessage(oHTTP.Get('https://api.example.com/v1/orders'));
finally
oHTTP.Free;
end;
end;
De intervallen zijn hier in milliseconden en niet in seconden, omdat HTTP-retries op een veel kortere tijdschaal leven dan een reconnect. De vertragingen volgen dezelfde curve als bij de WatchDog: 500ms, 1s, 2s, 4s, afgekapt op MaxInterval en gespreid door Jitter.
StatusCodes is de lijst met HTTP-statuscodes die als herhaalbaar tellen, als een door komma's gescheiden string. De standaard is 429,502,503,504, de codes die "nu even niet, probeer het opnieuw" betekenen in plaats van "uw verzoek is verkeerd". Een 400 of een 401 wordt nooit opnieuw geprobeerd, want hetzelfde kapotte verzoek nog vier keer versturen helpt niemand. Fouten op verbindingsniveau, zoals een geweigerde socket of een read-timeout, worden ook opnieuw geprobeerd.
Hetzelfde object zit op TsgcHTTPAPI_client in sgcHTTP_API, de basisklasse van elke kant-en-klare REST-client in de bibliotheek. Zo is het retry-beleid op al die clients beschikbaar zonder plumbing per client:
// Any API client that descends from TsgcHTTPAPI_client
sgcHTTPAPI_Client1.RetryOptions.Enabled := True;
sgcHTTPAPI_Client1.RetryOptions.MaxRetries := 5;
sgcHTTPAPI_Client1.RetryOptions.StatusCodes := '429,500,502,503,504';
Retry-After: laat de server het tempo bepalen
Uw eigen backoff berekenen is gokwerk. Wanneer een server 429 of 503 teruggeeft, vertelt hij u vaak precies hoe lang u moet wachten, in de Retry-After-responsheader, ofwel als een aantal seconden ofwel als een HTTP-datum. Dat getal is geen suggestie. Het is de server die u vertelt wanneer hij u weer zal accepteren, en een client die dat negeert en op zijn eigen schema opnieuw probeert, wordt simpelweg opnieuw geweigerd.
HonorRetryAfter staat standaard op True. Wanneer de response een Retry-After-header meedraagt, wint die waarde van de berekende backoff, en wacht de client zo lang als de server gevraagd heeft:
oHTTP.RetryOptions.Enabled := True;
oHTTP.RetryOptions.HonorRetryAfter := True; // default True
oHTTP.RetryOptions.InitialInterval := 500;
oHTTP.RetryOptions.MaxRetries := 3;
// Server replies 429 with "Retry-After: 12".
// The computed backoff would have been 500ms, the client waits 12 seconds instead.
Beide vormen van de header worden geparseerd, de vorm met delta-seconden en de vorm met een HTTP-datum. Dit is precies wat OpenAI en Anthropic van hun clients vragen wanneer ze een 429 teruggeven, en het is het verschil tussen een client die herstelt van een rate limit en een client die rate limited blijft.
Retries op de AI-clients
De AI-clients zijn de plek waar retries hun geld waard zijn, want rate limits zijn daar routine in plaats van uitzondering. Elk van hen stelt zijn eigen retry-options-object beschikbaar op zijn *Options-property, zodat de instellingen zichtbaar zijn in de Object Inspector naast het model en de API-sleutel.
Op TsgcHTTP_API_OpenAI is het pad OpenAIOptions.RetryOptions, van de klasse TsgcHTTPOpenAIRetry_Options:
uses
sgcHTTP_API_OpenAI;
begin
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Enabled := True;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Retries := 3;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Wait := 10000; // ms, first delay
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.MaxInterval := 30000; // ms, ceiling
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Multiplier := 2.0;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.Jitter := 0.2;
sgcHTTPOpenAI1.OpenAIOptions.RetryOptions.HonorRetryAfter := True;
end;
Anthropic heeft dezelfde vorm onder AnthropicOptions.RetryOptions, en Gemini onder GeminiOptions.RetryOptions. Hetzelfde patroon bestaat op de Grok-, Mistral-, DeepSeek- en Ollama-clients, en op de Stripe- en Paddle-clients, waar een opnieuw verstuurde betaalaanroep een heel ander soort belangrijk is.
// Anthropic returns 429 and 529 under load, both are treated as retryable.
sgcHTTPAnthropic1.AnthropicOptions.RetryOptions.Enabled := True;
sgcHTTPAnthropic1.AnthropicOptions.RetryOptions.Retries := 3;
sgcHTTPAnthropic1.AnthropicOptions.RetryOptions.HonorRetryAfter := True;
Let op: de propertynamen op de AI-objecten zijn Retries en Wait in plaats van MaxRetries en InitialInterval, om consistent te blijven met de naamgeving die elders op die componenten al gebruikt wordt. Het gedrag is identiek, en intern configureren ze dezelfde TsgcIdHTTPRetry_Options op de onderliggende HTTP-client.
Weten waarom het mislukte: Connect en LastError
Een retry-lus is niet beter dan zijn foutafhandeling. Voordat u kunt beslissen of een fout het opnieuw proberen waard is, moet u weten wat de fout was, en tot nu toe was de enige manier om daar op een WebSocket-client achter te komen het aansluiten van een OnError-handler en het bericht ergens in een veld bewaren.
2026.7 voegt een Connect-overload toe die u de fout rechtstreeks aanreikt, in sgcWebSocket_Client_Base:
function Connect(const aTimeout: Integer = 10000): Boolean; overload;
function Connect(out AError: string; const aTimeout: Integer = 10000): Boolean; overload;
Zo kan een handmatige reconnect-lus de reden inspecteren en zelf beslissen:
var
vError: string;
begin
if not sgcWebSocketClient1.Connect(vError, 5000) then
begin
// A DNS failure or a refused socket is worth retrying.
// A rejected certificate or a 401 on the handshake is not.
Memo1.Lines.Add('connect failed: ' + vError);
Exit;
end;
end;
De laatste fout wordt ook op de component zelf bewaard. TsgcWSComponent_Base in sgcWebSocket_Classes publiceert een alleen-lezen LastError: string-property en een ClearLastError-methode, zodat u de reden achteraf kunt lezen, en hem kunt resetten vóór een poging die u wilt meten:
sgcWebSocketClient1.ClearLastError;
if not sgcWebSocketClient1.Connect(5000) then
ShowMessage('connect failed: ' + sgcWebSocketClient1.LastError);
Niets hiervan vervangt OnError, dat nog steeds afgaat zoals altijd. Het betekent alleen dat een eenvoudig, synchroon stuk code niet langer een event handler hoeft op te zetten om een eenvoudige vraag te beantwoorden.
Beschikbaarheid
WatchDog-backoff, jitter, HTTP-retries en de nieuwe Connect-overload zijn beschikbaar vanaf sgcWebSockets 2026.7 in Delphi 7 tot en met 13 en C++Builder, op Win32/Win64, Linux64, macOS, Android en iOS. Alles wat hier beschreven is staat standaard uit. Backoff is wdbFixed, Jitter is 0 en RetryOptions.Enabled is False, dus een bestaande toepassing upgradet zonder enige gedragsverandering en u schakelt in waar het zinvol is.
Klanten met een actief abonnement kunnen de nieuwe build downloaden in het klantengedeelte. Trialgebruikers kunnen het bijgewerkte installatieprogramma ophalen op esegece.com/products/websockets/download.
Vragen, feedback of hulp bij het kiezen van een backoff-profiel voor uw vloot? Neem contact op, u krijgt antwoord van de mensen die de code hebben geschreven.
