Starten Sie einen WebSocket-Server neu, an dem zehntausend Clients hängen, und es passiert etwas Unangenehmes. Jeder Client bemerkt die Trennung ungefähr im selben Moment, jeder Client wartet dieselbe feste Anzahl an Sekunden, und jeder Client wählt sich im selben Takt wieder ein. Der Server kommt hoch und wird sofort von zehntausend gleichzeitigen TLS-Handshakes getroffen. Er fällt um, oder er drosselt, und die Clients versuchen es im Gleichschritt erneut. Das ist die Thundering Herd, und ein festes Reconnect-Intervall garantiert sie.
sgcWebSockets 2026.7 kümmert sich um die beiden Stellen, an denen Clients einen ohnehin schon angeschlagenen Server bombardieren. Der WatchDog kann sich jetzt mit exponentiellem Backoff plus Jitter statt mit einem festen Intervall neu verbinden, und der HTTP-Client kann eine fehlgeschlagene Anfrage selbst wiederholen, wobei er den Retry-After-Hinweis des Servers beachtet und dieselbe Backoff-Rechnung verwendet. Beides ist optional und standardmäßig deaktiviert, bestehende Anwendungen verhalten sich also genau wie zuvor, bis Sie es einschalten.
Das Problem mit einem festen Reconnect-Intervall
Der WatchDog war schon immer der übliche Weg, einen Client verbunden zu halten. Sie setzen Enabled, Sie setzen ein Interval in Sekunden, und wenn die Verbindung abbricht, versucht die Komponente es alle Interval Sekunden erneut, bis sie wieder steht.
Für einen einzelnen Client an einer wackeligen WLAN-Verbindung ist das in Ordnung. Für eine ganze Flotte ist es das falsche Verhalten. Mit einem festen Intervall ist jeder Client der Flotte ein Metronom, das im selben Takt tickt, und ein Server-Neustart synchronisiert sie alle: Sie wurden alle im selben Augenblick getrennt, also verbinden sie sich alle im selben Augenblick neu, für immer. Schlimmer noch, das Intervall passt sich nicht an. Ist der Server zehn Minuten lang weg, unternimmt ein Client mit Interval = 1 sechshundert sinnlose Verbindungsversuche, und alle anderen tun dasselbe.
Was Sie wollen, ist das Gegenteil. Versuchen Sie es beim ersten Mal schnell erneut, denn die meisten Trennungen sind vorübergehend und ein schneller Reconnect bleibt für den Benutzer unsichtbar. Ziehen Sie sich danach zurück, damit ein Server, der wirklich down ist, nicht weiter beharkt wird. Und verteilen Sie die Clients, damit sie nicht alle gleichzeitig ankommen.
Exponentielles Backoff im WatchDog
Das Optionsobjekt des WatchDog hat vier neue Eigenschaften bekommen. Der Typ liegt in sgcTCP_Classes:
type
TsgcWatchDogBackoff = (wdbFixed, wdbExponential);
Auf jeder WebSocket-Komponente sind die Optionen über Client.WatchDog erreichbar, und die Backoff-Einstellungen sind diese:
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;
Die Rechnung ist bewusst langweilig. Der erste Reconnect-Versuch wartet Interval. Jeder folgende Versuch multipliziert die vorherige Verzögerung mit BackoffMultiplier, und das Ergebnis wird auf MaxInterval begrenzt. Mit den Werten von oben lauten die Verzögerungen 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, 60s und so weiter. Der Zähler wird zurückgesetzt, sobald der Client wieder verbunden ist, ein kurzer Netzwerkaussetzer kostet Sie also eine Sekunde, nicht eine Minute.
Interval und MaxInterval sind beide in Sekunden angegeben, so wie es beim WatchDog schon immer war. Ein MaxInterval von 0 bedeutet keine Obergrenze, und das ist der Standardwert. Wenn Sie exponentielles Backoff aktivieren, setzen Sie eine Obergrenze, sonst verdoppelt sich die Verzögerung immer weiter, bis sie in Stunden gemessen wird.
Das wichtige Detail für bestehende Anwender: Backoff ist standardmäßig wdbFixed. Wenn Sie aktualisieren und nichts ändern, verbindet sich Ihr Client mit demselben festen Intervall neu wie eh und je. Die Verzögerungsberechnung wird nur verwendet, wenn Sie auf wdbExponential umschalten oder einen Jitter setzen, und es ist derselbe Helfer, den auch die HTTP-Wiederholung nutzt, sgcNextBackoffMs in sgcBase_Helpers.
Jitter, und warum er in großem Maßstab zählt
Exponentielles Backoff allein bricht die Herde nicht auf. Es sorgt nur dafür, dass die Herde seltener ankommt. Zehntausend Clients, die alle 1s, dann 2s, dann 4s warten, sind immer noch zehntausend Clients, die gemeinsam ankommen, nur eben in immer größeren Wellen.
Jitter ist das, was sie tatsächlich verteilt. Er ist ein Bruchteil der berechneten Verzögerung, zufällig pro Client und pro Versuch angewandt. Mit Jitter = 0.2 wird aus einer nominalen Verzögerung von 8 Sekunden ein zufälliger Wert in einem 20-Prozent-Band um 8 Sekunden herum, Client A wacht also bei 7,1s auf, Client B bei 8,6s, Client C bei 8,0s. Die Flotte kommt verschmiert statt als Spitze an, und die Accept-Queue des Servers kommt mit.
// 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 ist unabhängig vom Backoff-Modus. Sie können Backoff auf wdbFixed lassen und nur Jitter setzen, was Ihnen das klassische feste Intervall mit desynchronisierten Clients gibt. Das genügt oft schon für sich allein, und es ist die kleinstmögliche Änderung an einer bestehenden Anwendung.
Eine HTTP-Anfrage automatisch wiederholen
Die zweite Hälfte des Features liegt auf der HTTP-Seite. Eine einzelne HTTP-Anfrage, die mit einem 503 scheitert, weil der Server gerade neu ausgerollt wird, oder mit einem 429, weil Sie ein Rate Limit überschritten haben, ist nicht wirklich ein Fehler. Es ist eine Anfrage, die gleich noch einmal gesendet werden sollte. Diese Schleife von Hand zu schreiben ist mühsam, und jeder schreibt sie ein bisschen falsch.
TsgcIdHTTP hat jetzt ein RetryOptions-Objekt, deklariert in sgcHTTP_Client als TsgcIdHTTPRetry_Options. Aktivieren Sie es, und die Anfrage wiederholt sich selbst:
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;
Die Intervalle sind hier in Millisekunden angegeben, nicht in Sekunden, denn HTTP-Wiederholungen bewegen sich auf einer viel kürzeren Zeitskala als ein Reconnect. Die Verzögerungen folgen derselben Kurve wie beim WatchDog: 500ms, 1s, 2s, 4s, begrenzt durch MaxInterval und gestreut durch Jitter.
StatusCodes ist die Liste der HTTP-Statuscodes, die als wiederholbar gelten, als kommaseparierter String. Der Standardwert ist 429,502,503,504, also die Codes, die "nicht jetzt, versuch es später noch einmal" bedeuten, und nicht "deine Anfrage ist falsch". Ein 400 oder ein 401 wird nie wiederholt, denn dieselbe kaputte Anfrage noch vier weitere Male zu senden hilft niemandem. Fehler auf Verbindungsebene wie ein abgelehnter Socket oder ein Lese-Timeout werden ebenfalls wiederholt.
Dasselbe Objekt liegt auf TsgcHTTPAPI_client in sgcHTTP_API, der Basisklasse jedes fertigen REST-Clients in der Bibliothek. Die Retry-Richtlinie steht damit auf allen von ihnen zur Verfügung, ganz ohne Verkabelung pro 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: den Server das Tempo bestimmen lassen
Ein eigenes Backoff zu berechnen ist Raterei. Wenn ein Server einen 429 oder 503 zurückgibt, sagt er Ihnen oft ganz genau, wie lange Sie warten sollen, nämlich im Retry-After-Antwortheader, entweder als Anzahl von Sekunden oder als HTTP-Datum. Diese Zahl ist kein Vorschlag. Es ist der Server, der Ihnen sagt, wann er Sie wieder annehmen wird, und ein Client, der das ignoriert und nach seinem eigenen Zeitplan wiederholt, wird schlicht erneut abgewiesen.
HonorRetryAfter ist standardmäßig True. Trägt die Antwort einen Retry-After-Header, gewinnt dieser Wert gegenüber dem berechneten Backoff, und der Client wartet so lange, wie der Server es verlangt hat:
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 Formen des Headers werden geparst, die Form mit Delta-Sekunden und die Form mit HTTP-Datum. Genau das verlangen OpenAI und Anthropic von ihren Clients, wenn sie einen 429 zurückgeben, und es ist der Unterschied zwischen einem Client, der sich von einem Rate Limit erholt, und einem Client, der im Rate Limit stecken bleibt.
Wiederholungen auf den AI-Clients
Bei den AI-Clients zahlen sich Wiederholungen am meisten aus, denn Rate Limits sind dort Alltag statt Ausnahme. Jeder von ihnen stellt ein eigenes Retry-Optionsobjekt auf seiner *Options-Eigenschaft bereit, die Einstellungen sind also im Objektinspektor neben dem Modell und dem API-Schlüssel sichtbar.
Auf TsgcHTTP_API_OpenAI lautet der Pfad OpenAIOptions.RetryOptions, von der 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 hat dieselbe Form unter AnthropicOptions.RetryOptions und Gemini unter GeminiOptions.RetryOptions. Dasselbe Muster gibt es auf den Clients für Grok, Mistral, DeepSeek und Ollama sowie auf den Clients für Stripe und Paddle, wo ein wiederholter Zahlungsaufruf eine ganz andere Art von Wichtigkeit hat.
// 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;
Beachten Sie, dass die Eigenschaften auf den AI-Objekten Retries und Wait heißen und nicht MaxRetries und InitialInterval, um bei der Namensgebung konsistent zu bleiben, die auf diesen Komponenten ohnehin schon verwendet wird. Das Verhalten ist identisch, und intern konfigurieren sie dieselben TsgcIdHTTPRetry_Options auf dem zugrundeliegenden HTTP-Client.
Wissen, warum es fehlgeschlagen ist: Connect und LastError
Eine Retry-Schleife ist nur so gut wie ihre Fehlerbehandlung. Bevor Sie entscheiden können, ob ein Fehlschlag eine Wiederholung wert ist, müssen Sie wissen, worin der Fehlschlag bestand, und bis jetzt war der einzige Weg, das auf einem WebSocket-Client herauszufinden, einen OnError-Handler zu verdrahten und die Meldung irgendwo in einem Feld abzulegen.
2026.7 ergänzt eine Connect-Überladung, die Ihnen den Fehler direkt aushändigt, in sgcWebSocket_Client_Base:
function Connect(const aTimeout: Integer = 10000): Boolean; overload;
function Connect(out AError: string; const aTimeout: Integer = 10000): Boolean; overload;
Eine manuelle Reconnect-Schleife kann den Grund also prüfen und selbst entscheiden:
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;
Der letzte Fehler wird außerdem auf der Komponente selbst festgehalten. TsgcWSComponent_Base in sgcWebSocket_Classes veröffentlicht eine schreibgeschützte Eigenschaft LastError: string und eine Methode ClearLastError, Sie können den Grund also im Nachhinein auslesen und ihn vor einem Versuch zurücksetzen, den Sie messen wollen:
sgcWebSocketClient1.ClearLastError;
if not sgcWebSocketClient1.Connect(5000) then
ShowMessage('connect failed: ' + sgcWebSocketClient1.LastError);
Nichts davon ersetzt OnError, das weiterhin so ausgelöst wird wie eh und je. Es bedeutet nur, dass ein einfaches, synchrones Stück Code nicht mehr einen Ereignishandler aufsetzen muss, um eine einfache Frage zu beantworten.
Verfügbarkeit
WatchDog-Backoff, Jitter, HTTP-Wiederholungen und die neue Connect-Überladung stehen ab sgcWebSockets 2026.7 für Delphi 7 bis 13 und C++Builder zur Verfügung, unter Win32/Win64, Linux64, macOS, Android und iOS. Alles hier Beschriebene ist standardmäßig deaktiviert. Backoff ist wdbFixed, Jitter ist 0 und RetryOptions.Enabled ist False, eine bestehende Anwendung lässt sich also ganz ohne Verhaltensänderung aktualisieren, und Sie schalten dort zu, wo es sinnvoll ist.
Kunden mit einem aktiven Abonnement können den neuen Build im Kundenbereich herunterladen. Trial-Nutzer erhalten das aktualisierte Installationsprogramm unter esegece.com/products/websockets/download.
Fragen, Feedback oder Hilfe bei der Wahl eines Backoff-Profils für Ihre Flotte? Nehmen Sie Kontakt auf, Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.
