Reinicie um servidor WebSocket com dez mil clientes conectados e algo desagradável acontece. Todos os clientes percebem a desconexão praticamente no mesmo instante, todos esperam o mesmo número fixo de segundos e todos discam de volta no mesmo tique. O servidor sobe e é imediatamente atingido por dez mil handshakes TLS simultâneos. Ele cai, ou aplica limites, e os clientes tentam de novo em uníssono. Isso é a manada em disparada, e um intervalo fixo de reconexão a garante.
O sgcWebSockets 2026.7 trata dos dois pontos em que os clientes martelam um servidor que já está em apuros. O WatchDog agora pode reconectar com recuo exponencial mais jitter em vez de um intervalo fixo, e o cliente HTTP pode repetir sozinho uma requisição que falhou, respeitando a dica Retry-After do servidor, usando a mesma matemática de recuo. Ambos são opcionais e estão desativados por padrão, de modo que as aplicações existentes se comportam exatamente como antes até que você os habilite.
O problema com um intervalo fixo de reconexão
O WatchDog sempre foi a forma padrão de manter um cliente conectado. Você define Enabled, define um Interval em segundos, e quando a conexão cai o componente tenta de novo a cada Interval segundos até voltar.
Isso serve para um cliente em um link Wi-Fi instável. É o comportamento errado para uma frota. Com um intervalo fixo, cada cliente da frota é um metrônomo batendo no mesmo ritmo, e uma reinicialização do servidor sincroniza todos eles: todos desconectaram no mesmo instante, então todos reconectam no mesmo instante, para sempre. Pior, o intervalo não se adapta. Se o servidor ficar fora do ar por dez minutos, um cliente com Interval = 1 faz seiscentas tentativas de conexão inúteis, e todos os outros também.
O que você quer é o oposto. Tente de novo rapidamente da primeira vez, porque a maioria das desconexões é passageira e uma reconexão rápida é invisível para o usuário. Depois recue, para que um servidor que realmente está fora do ar não seja martelado. E espalhe os clientes, para que não cheguem todos juntos.
Recuo exponencial no WatchDog
O objeto de opções do WatchDog ganhou quatro propriedades. O tipo vive em sgcTCP_Classes:
type
TsgcWatchDogBackoff = (wdbFixed, wdbExponential);
Em qualquer componente WebSocket as opções são acessadas como Client.WatchDog, e as configurações de recuo são estas:
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;
A matemática é deliberadamente banal. A primeira tentativa de reconexão espera Interval. Cada tentativa seguinte multiplica o atraso anterior por BackoffMultiplier, e o resultado é limitado a MaxInterval. Com os valores acima, os atrasos vão de 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, 60s e assim por diante. O contador é reiniciado assim que o cliente está conectado de novo, então uma pequena falha de rede custa a você um segundo, não um minuto.
Interval e MaxInterval são ambos expressos em segundos, como o WatchDog sempre foi. Um MaxInterval de 0 significa sem teto, que é o padrão. Se você habilitar o recuo exponencial, defina um teto, caso contrário o atraso continua dobrando até ser medido em horas.
O detalhe importante para os usuários atuais: Backoff tem wdbFixed como padrão. Se você atualizar e não mudar nada, o seu cliente reconecta no mesmo intervalo fixo de sempre. O cálculo do atraso só é usado quando você muda para wdbExponential ou define um Jitter, e é o mesmo auxiliar que a nova tentativa HTTP usa, sgcNextBackoffMs em sgcBase_Helpers.
Jitter, e por que ele importa em escala
O recuo exponencial sozinho não quebra a manada. Ele só faz a manada chegar com menos frequência. Dez mil clientes que esperam todos 1s, depois 2s, depois 4s continuam sendo dez mil clientes chegando juntos, apenas em ondas cada vez maiores.
Jitter é o que de fato os espalha. É uma fração do atraso calculado, aplicada aleatoriamente por cliente e por tentativa. Com Jitter = 0.2 um atraso nominal de 8 segundos vira um valor aleatório em uma faixa de 20% em torno de 8 segundos, então o cliente A acorda aos 7,1s, o cliente B aos 8,6s, o cliente C aos 8,0s. A frota chega como uma mancha em vez de um pico, e a fila de aceitação do servidor consegue dar conta.
// 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
O jitter é ortogonal ao modo de recuo. Você pode deixar Backoff em wdbFixed e definir apenas Jitter, o que lhe dá o clássico intervalo fixo com os clientes dessincronizados. Isso muitas vezes já basta por si só, e é a menor mudança possível em uma aplicação existente.
Repetindo uma requisição HTTP automaticamente
A segunda metade do recurso está do lado do HTTP. Uma única requisição HTTP que falha com um 503 porque o servidor está sendo reimplantado, ou com um 429 porque você passou de um limite de taxa, não é realmente um erro. É uma requisição que deveria ser enviada de novo daqui a pouco. Escrever esse laço à mão é tedioso e todo mundo o escreve um pouco errado.
TsgcIdHTTP agora tem um objeto RetryOptions, declarado em sgcHTTP_Client como TsgcIdHTTPRetry_Options. Habilite-o e a requisição se repete sozinha:
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;
Os intervalos aqui são em milissegundos, não em segundos, porque as novas tentativas HTTP vivem em uma escala de tempo muito mais curta do que uma reconexão. Os atrasos seguem a mesma curva do WatchDog: 500ms, 1s, 2s, 4s, limitados por MaxInterval e espalhados por Jitter.
StatusCodes é a lista de códigos de status HTTP que contam como passíveis de nova tentativa, na forma de uma string separada por vírgulas. O padrão é 429,502,503,504, que são os códigos que significam "agora não, tente de novo" em vez de "sua requisição está errada". Um 400 ou um 401 nunca é repetido, porque enviar a mesma requisição quebrada mais quatro vezes não ajuda ninguém. Falhas no nível da conexão, como um socket recusado ou um tempo limite de leitura, também são repetidas.
O mesmo objeto está em TsgcHTTPAPI_client, em sgcHTTP_API, que é a classe base de todo cliente REST pronto para uso da biblioteca. Assim a política de novas tentativas está disponível em todos eles sem nenhuma ligação por cliente:
// 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: deixe o servidor ditar o ritmo
Calcular o seu próprio recuo é um chute. Quando um servidor devolve 429 ou 503 ele muitas vezes diz exatamente quanto tempo esperar, no cabeçalho de resposta Retry-After, seja como um número de segundos, seja como uma data HTTP. Esse número não é uma sugestão. É o servidor dizendo quando vai aceitar você de novo, e um cliente que o ignora e tenta de novo no seu próprio horário simplesmente vai ser rejeitado outra vez.
HonorRetryAfter é True por padrão. Quando a resposta traz um cabeçalho Retry-After, esse valor prevalece sobre o recuo calculado, e o cliente espera o tempo que o servidor pediu:
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.
As duas formas do cabeçalho são interpretadas, a forma de segundos de diferença e a forma de data HTTP. Isso é exatamente o que a OpenAI e a Anthropic pedem que seus clientes façam quando devolvem um 429, e é a diferença entre um cliente que se recupera de um limite de taxa e um cliente que continua limitado.
Novas tentativas nos clientes de IA
Os clientes de IA são onde as novas tentativas valem o seu preço, porque ali os limites de taxa são rotina em vez de exceção. Cada um deles expõe o seu próprio objeto de opções de nova tentativa na sua propriedade *Options, então as configurações ficam visíveis no Object Inspector ao lado do modelo e da chave de API.
Em TsgcHTTP_API_OpenAI o caminho é OpenAIOptions.RetryOptions, da classe 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;
A Anthropic tem o mesmo formato em AnthropicOptions.RetryOptions, e o Gemini em GeminiOptions.RetryOptions. O mesmo padrão existe nos clientes Grok, Mistral, DeepSeek e Ollama, e nos clientes Stripe e Paddle, onde uma chamada de pagamento repetida é um tipo de importância bem diferente.
// 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;
Note que os nomes das propriedades nos objetos de IA são Retries e Wait em vez de MaxRetries e InitialInterval, para manter a consistência com a nomenclatura já usada em outras partes desses componentes. O comportamento é idêntico, e internamente eles configuram o mesmo TsgcIdHTTPRetry_Options no cliente HTTP subjacente.
Saber por que falhou: Connect e LastError
Um laço de novas tentativas é tão bom quanto o seu tratamento de erros. Antes de poder decidir se uma falha vale uma nova tentativa, você precisa saber qual foi a falha, e até agora a única maneira de descobrir isso em um cliente WebSocket era ligar um manipulador OnError e guardar a mensagem em algum campo.
A 2026.7 adiciona uma sobrecarga de Connect que entrega o erro diretamente, em sgcWebSocket_Client_Base:
function Connect(const aTimeout: Integer = 10000): Boolean; overload;
function Connect(out AError: string; const aTimeout: Integer = 10000): Boolean; overload;
Assim um laço manual de reconexão pode inspecionar o motivo e decidir por conta própria:
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;
O último erro também é mantido no próprio componente. TsgcWSComponent_Base, em sgcWebSocket_Classes, publica uma propriedade somente leitura LastError: string e um método ClearLastError, então você pode ler o motivo depois do fato e reiniciá-lo antes de uma tentativa que queira medir:
sgcWebSocketClient1.ClearLastError;
if not sgcWebSocketClient1.Connect(5000) then
ShowMessage('connect failed: ' + sgcWebSocketClient1.LastError);
Nada disso substitui o OnError, que continua disparando como sempre. Apenas significa que um trecho de código simples e síncrono não precisa mais montar um manipulador de eventos para responder a uma pergunta simples.
Disponibilidade
O recuo do WatchDog, o jitter, as novas tentativas HTTP e a nova sobrecarga de Connect estão disponíveis a partir do sgcWebSockets 2026.7 em Delphi 7 até 13 e C++Builder, em Win32/Win64, Linux64, macOS, Android e iOS. Tudo o que foi descrito aqui está desativado por padrão. Backoff é wdbFixed, Jitter é 0 e RetryOptions.Enabled é False, de modo que uma aplicação existente é atualizada sem nenhuma mudança de comportamento e você habilita o que fizer sentido.
Clientes com assinatura ativa podem baixar a nova versão na área de clientes. Usuários de avaliação podem obter o instalador atualizado em esegece.com/products/websockets/download.
Dúvidas, comentários ou ajuda para escolher um perfil de recuo para a sua frota? Entre em contato, você receberá uma resposta das pessoas que escreveram o código.
