Para usar TCP keepalive em Zig, habilite SO_KEEPALIVE no file descriptor do socket e, quando a plataforma permitir, ajuste TCP_KEEPIDLE, TCP_KEEPINTVL e TCP_KEEPCNT. Mas não trate isso como substituto de timeout: keepalive responde à pergunta “o outro lado ainda está alcançável depois de ficar ocioso?”, enquanto um deadline da aplicação responde “esta operação terminou dentro do tempo aceitável?”. Em serviços de produção, normalmente você precisa dos dois.
O problema aparece em conexões longas com banco de dados, filas, agentes, WebSockets, RPC, túneis e protocolos próprios. Um notebook perde a rede, um container desaparece, um roteador descarta estado ou uma máquina desliga abruptamente. Como não houve FIN nem RST, o socket local pode continuar parecendo aberto até a próxima escrita, até um timeout intermediário ou até o kernel concluir que o peer morreu.
Este guia complementa o tutorial de sockets TCP e UDP em Zig, o conteúdo sobre DNS, IPv4 e IPv6 e o padrão de timeout, retry e circuit breaker. Como Zig ainda evolui antes do 1.0, confirme os nomes exatos de std.posix e das constantes na versão fixada pelo projeto.
Resposta rápida
| Necessidade | Mecanismo recomendado |
|---|---|
| Detectar peer que desapareceu sem fechar TCP | SO_KEEPALIVE + parâmetros da plataforma |
| Limitar tempo de uma resposta | deadline ou timeout da aplicação |
| Manter WebSocket/SSE observável | ping/pong ou heartbeat no protocolo |
| Impedir conexão ociosa eterna | idle timeout da aplicação |
| Reutilizar conexão HTTP | HTTP keep-alive, que é outro conceito |
| Sobreviver a idle timeout de proxy | alinhar limites e usar heartbeat quando necessário |
| Linux com detecção mais rápida | TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT |
| Portabilidade | helper por plataforma e política comum acima dele |
| Teste realista | perda silenciosa de pacotes, não apenas kill normal |
| Produção | métricas, jitter, limites e reconexão com backoff |
O que é uma conexão TCP “morta”
TCP mantém estado nas duas pontas. Quando um programa fecha a conexão normalmente, o kernel envia FIN; quando há uma falha explícita, pode chegar um RST. Nesses casos, uma leitura retorna fim de arquivo ou uma operação informa erro.
A situação difícil é a falha silenciosa:
- a máquina cliente perde energia;
- a interface Wi-Fi troca de rede;
- um NAT remove o mapeamento;
- uma rota fica em blackhole;
- um firewall passa a descartar os pacotes;
- um container é removido junto com seu namespace;
- o peer continua vivo, mas não existe mais caminho entre as pontas.
Sem tráfego, o socket local não recebe informação nova. Para o kernel, a conexão pode continuar estabelecida. TCP keepalive introduz sondas depois de um intervalo ocioso; se nenhuma resposta chega após as tentativas configuradas, o kernel considera a conexão perdida e as operações seguintes observam o erro.
Isso é diferente de uma aplicação lenta. O peer pode responder às sondas TCP e, mesmo assim, nunca entregar a resposta de negócio. O kernel sabe que a pilha TCP remota está viva, mas não sabe se uma query travou, se um worker entrou em deadlock ou se uma fila ficou congestionada.
As quatro opções principais no Linux
Em Linux, a política costuma ser formada por quatro opções:
SO_KEEPALIVE: liga o mecanismo naquele socket;TCP_KEEPIDLE: segundos de ociosidade antes da primeira sonda;TCP_KEEPINTVL: intervalo entre sondas sem resposta;TCP_KEEPCNT: quantidade de sondas malsucedidas antes de desistir.
Um perfil como idle=60, interval=10 e count=3 começa a sondar após aproximadamente 60 segundos sem tráfego e pode concluir a falha depois de mais algumas dezenas de segundos. Esse cálculo é uma aproximação operacional, não um SLA rígido: escalonamento do kernel, retransmissões, diferenças de plataforma e estado da rede influenciam o momento observado.
Os padrões globais do Linux podem ser consultados com:
sysctl net.ipv4.tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_intvl
sysctl net.ipv4.tcp_keepalive_probes
Não altere os valores globais só para corrigir um serviço. Uma mudança via sysctl afeta outras aplicações do host e pode criar tráfego ou comportamento inesperado. Quando possível, configure a política por socket.
Habilitando keepalive em Zig
As APIs de alto nível de std.net são adequadas para conectar, aceitar, ler e escrever. Opções específicas do sistema operacional normalmente exigem acessar o handle do stream e chamar std.posix.setsockopt.
O helper abaixo mostra o desenho para Linux. Os tipos e nomes das constantes podem variar entre releases de Zig, então compile contra a versão do seu projeto e ajuste a conversão do valor conforme a assinatura disponível:
const std = @import("std");
const KeepaliveConfig = struct {
idle_seconds: u32 = 60,
interval_seconds: u32 = 10,
probe_count: u32 = 3,
};
fn setIntOption(
fd: std.posix.socket_t,
level: u32,
option: u32,
value: u32,
) !void {
const bytes = std.mem.asBytes(&value);
try std.posix.setsockopt(fd, level, option, bytes);
}
fn configureKeepalive(
stream: std.net.Stream,
config: KeepaliveConfig,
) !void {
try setIntOption(
stream.handle,
std.posix.SOL.SOCKET,
std.posix.SO.KEEPALIVE,
1,
);
if (@import("builtin").os.tag == .linux) {
try setIntOption(
stream.handle,
std.posix.IPPROTO.TCP,
std.posix.TCP.KEEPIDLE,
config.idle_seconds,
);
try setIntOption(
stream.handle,
std.posix.IPPROTO.TCP,
std.posix.TCP.KEEPINTVL,
config.interval_seconds,
);
try setIntOption(
stream.handle,
std.posix.IPPROTO.TCP,
std.posix.TCP.KEEPCNT,
config.probe_count,
);
}
}
A ideia importante é separar política e adaptação da plataforma. O restante do programa deveria pedir algo como “detectar peer ausente em cerca de dois minutos”, sem espalhar constantes Linux por todo o código.
Em algumas versões, SOL, SO, TCP ou IPPROTO têm outra organização, e o tipo esperado por setsockopt pode ser []const u8 construído de maneira diferente. Consulte a implementação da stdlib instalada:
zig env
rg "pub fn setsockopt|KEEPIDLE|KEEPINTVL|KEEPCNT" /caminho/da/lib/std
O objetivo não é esconder a variação: é confiná-la em um módulo pequeno, testável e condicionado por target.
Aplicando no cliente TCP
Configure a opção logo depois de abrir a conexão e antes de entregá-la à camada que fará I/O:
const std = @import("std");
pub fn main() !void {
const address = try std.net.Address.parseIp("203.0.113.10", 9000);
const stream = try std.net.tcpConnectToAddress(address);
defer stream.close();
try configureKeepalive(stream, .{
.idle_seconds = 60,
.interval_seconds = 10,
.probe_count = 3,
});
try stream.writeAll("HELLO\n");
var buffer: [4096]u8 = undefined;
const n = try stream.read(&buffer);
std.debug.print("resposta: {s}\n", .{buffer[0..n]});
}
Se a conexão é criada por um pool, o lugar correto para configurar keepalive é a fábrica de conexões do pool. Assim, nenhuma conexão escapa com política diferente. Quando o pool substitui um socket quebrado, a nova conexão também recebe as opções.
Aplicando em conexões aceitas pelo servidor
O servidor pode configurar cada stream retornado por accept:
while (true) {
const connection = try server.accept();
configureKeepalive(connection.stream, .{}) catch |err| {
std.log.err("keepalive_config_failed error={}", .{err});
connection.stream.close();
continue;
};
const thread = std.Thread.spawn(
.{},
handleClient,
.{connection.stream},
) catch |err| {
std.log.err("spawn_failed error={}", .{err});
connection.stream.close();
continue;
};
thread.detach();
}
Decida conscientemente se falhar ao configurar keepalive deve rejeitar a conexão. Para um daemon Linux que depende desse mecanismo, continuar sem ele pode criar conexões zumbis. Para uma CLI multiplataforma, talvez seja melhor registrar a limitação e continuar com deadlines de aplicação. Não ignore o erro silenciosamente.
Keepalive não é timeout de leitura
Considere uma requisição que deve responder em até cinco segundos. O peer está vivo, responde a sondas TCP, mas o handler remoto leva três minutos. Keepalive não encerra a operação porque a conexão continua saudável na camada de transporte.
A política completa costuma ter camadas:
- connect timeout limita DNS e estabelecimento da conexão;
- request deadline limita a operação de negócio inteira;
- read/write timeout evita I/O bloqueado além do permitido;
- idle timeout fecha sessões sem atividade útil;
- TCP keepalive detecta peers desaparecidos em conexões ociosas;
- retry com backoff controla a recuperação após falha;
- circuit breaker reduz pressão sobre uma dependência instável.
Não some limites sem pensar no orçamento total. Se o usuário tem deadline de dois segundos, não faz sentido permitir três retries de dois segundos cada. Propague um prazo absoluto e calcule o tempo restante antes de cada fase.
TCP keepalive, HTTP keep-alive e heartbeat
Os nomes parecidos causam erros de arquitetura.
HTTP keep-alive significa reutilizar a mesma conexão TCP para múltiplas requisições. Ele reduz handshakes e latência. Em HTTP/1.1, conexões persistentes são comuns; HTTP/2 multiplexa streams sobre uma conexão.
TCP keepalive é uma sonda do kernel após ociosidade. O payload e o protocolo da aplicação não participam da decisão.
Heartbeat de aplicação é uma mensagem real do protocolo, como ping/pong de WebSocket ou um frame periódico próprio. Ele pode provar que o event loop e a lógica remota ainda estão funcionando, além de produzir métricas compreensíveis pelo serviço.
Para um servidor WebSocket em Zig, ping/pong normalmente é mais útil para presença e latência. TCP keepalive ainda pode atuar como rede de segurança, principalmente quando nenhuma mensagem de aplicação é esperada por longos períodos.
Proxies, NAT e load balancers
Um socket pode estar saudável nas duas aplicações e, ainda assim, desaparecer em um equipamento intermediário. NATs, firewalls, Nginx e load balancers mantêm estado e aplicam seus próprios limites de ociosidade.
Antes de escolher os números:
- descubra o idle timeout do caminho real;
- mantenha o heartbeat abaixo desse limite, com margem;
- evite sincronizar milhares de clientes no mesmo segundo;
- aplique jitter quando a aplicação controla o heartbeat;
- alinhe timeout de proxy, servidor e cliente;
- documente quem deve iniciar a reconexão.
Sondas TCP podem ou não renovar o estado considerado pelo intermediário. Não dependa desse efeito sem testar o produto e a configuração específica. Se a sessão precisa permanecer aberta, um heartbeat visível na camada de aplicação é mais explícito.
Em uma API Zig atrás de Nginx como proxy reverso, lembre que existem duas conexões: cliente–Nginx e Nginx–upstream. Cada trecho tem opções, timeouts e falhas independentes.
Como escolher valores
Não existe um valor universal. Comece pelo impacto de uma conexão fantasma e pelo número de sockets simultâneos.
| Cenário | Ponto de partida para avaliar |
|---|---|
| Serviço interno com requests frequentes | keepalive conservador; deadline faz a maior parte do trabalho |
| Agente conectado continuamente | idle de 30–120 s e detecção em poucas sondas, após teste |
| Pool de banco de dados | respeitar recomendações do driver, servidor e infraestrutura |
| Milhares de dispositivos | intervalos maiores, jitter de heartbeat e orçamento de bateria/rede |
| WebSocket interativo | ping/pong de aplicação + keepalive complementar |
| Job curto ou CLI | talvez keepalive não ajude; connect/request timeout é prioritário |
Valores agressivos aumentam sondas, logs e reconexões durante instabilidades breves. Valores longos preservam recursos de rede, mas mantêm estado inútil por mais tempo. Meça conexões simultâneas, taxa de reconexão e duração até detectar falhas.
Como testar de verdade
Encerrar o servidor com shutdown normal não valida keepalive: o kernel provavelmente enviará FIN ou RST, e o cliente detectará imediatamente. Você precisa simular descarte silencioso.
Em laboratório Linux, uma sequência útil é:
- configurar valores curtos no socket de teste;
- estabelecer a conexão;
- bloquear pacotes do peer com firewall ou interromper a rota;
- não enviar tráfego de aplicação;
- observar estado e timers com
ss; - capturar sondas com
tcpdump; - medir até a leitura ou escrita retornar erro;
- remover a regra de bloqueio ao terminar.
Comandos de observação:
ss -tonp
sudo tcpdump -ni any 'tcp port 9000'
Use namespace de rede, VM ou container descartável para não alterar o firewall de uma máquina compartilhada. Um teste automatizado deve ter cleanup garantido mesmo quando falhar.
Também teste os casos que keepalive não resolve:
- peer vivo que nunca responde à mensagem;
- proxy encerrando por idle timeout;
- conexão recuperada depois de uma pausa curta;
- milhares de clientes reconectando juntos;
- mudança de DNS durante a reconexão;
- shutdown gracioso do servidor.
Observabilidade em produção
Não registre apenas ConnectionResetByPeer. A investigação melhora quando o log estruturado contém:
- destino lógico, sem expor credenciais;
- família IPv4 ou IPv6;
- fase: DNS, connect, handshake, leitura ou escrita;
- duração da conexão;
- tempo desde a última atividade da aplicação;
- política de keepalive aplicada;
- causa da reconexão;
- número da tentativa e backoff;
- deadline restante.
Métricas recomendadas:
- conexões abertas;
- conexões ociosas;
- encerramentos por timeout;
- erros de leitura e escrita por classe;
- reconexões por minuto;
- duração até recuperar uma sessão;
- falhas ao aplicar
setsockopt; - heartbeats enviados, recebidos e atrasados.
Cuidado com cardinalidade: não use IP completo, ID de cliente ou mensagem de erro arbitrária como label de métrica. Esses detalhes podem ficar em logs amostrados.
Portabilidade e design do módulo
Linux, macOS, BSD e Windows não oferecem exatamente o mesmo conjunto de opções e nomes. Uma API interna pode expor intenção em vez de constantes:
const ConnectionLiveness = struct {
enabled: bool = true,
idle_ns: u64,
interval_ns: u64,
attempts: u32,
};
A implementação por target converte unidades, verifica limites e informa capacidades. Se uma opção não existe, escolha entre:
- retornar erro de configuração;
- aplicar apenas
SO_KEEPALIVE; - usar heartbeat de aplicação;
- desabilitar o recurso com log explícito;
- impedir aquele target no perfil de produção.
Evite catch {} em configuração de socket. Um serviço pode passar meses acreditando que detecta conexões mortas rapidamente quando, na prática, usa o padrão de horas do sistema.
Checklist de produção
Antes do deploy:
- habilite
SO_KEEPALIVEapenas onde conexões longas justificam; - defina deadline separado para cada operação de negócio;
- conheça idle timeouts de proxy, NAT e load balancer;
- configure valores por socket em vez de mudar
sysctlglobal sem necessidade; - centralize diferenças de plataforma em um helper;
- trate erros de
setsockoptde forma explícita; - use heartbeat de aplicação quando precisar validar o processo remoto;
- adicione jitter a heartbeats controlados pela aplicação;
- reconecte com backoff e limite de tentativas;
- refaça DNS na reconexão quando o destino usa hostname;
- teste perda silenciosa, não só encerramento normal;
- monitore reconexões, sockets ociosos e duração das falhas;
- inclua keepalive no teste de carga com muitos sockets;
- combine com graceful shutdown para não transformar deploy em tempestade de erros.
Conclusão
TCP keepalive é uma ferramenta de detecção de liveness da conexão, não um timeout universal. Em Zig, o padrão robusto é abrir ou aceitar o stream com std.net, configurar opções específicas por meio de uma camada pequena de std.posix e manter deadlines, idle timeout e heartbeat na camada apropriada.
Para produção, comece pela pergunta operacional: quanto tempo uma conexão fantasma pode consumir recursos sem causar impacto? Depois alinhe kernel, protocolo, proxy e reconexão. Valores copiados sem entender a infraestrutura podem ser tão ruins quanto deixar os padrões do sistema.
Com SO_KEEPALIVE, parâmetros por socket, testes de blackhole e observabilidade, um cliente ou servidor Zig consegue detectar peers desaparecidos sem confundir saúde do TCP com saúde da aplicação.
Perguntas frequentes
Para que serve TCP keepalive em uma aplicação Zig?
Ele ajuda o kernel a detectar uma conexão que ficou ociosa e cujo peer desapareceu sem enviar FIN ou RST. Após as sondas sem resposta, o socket passa a reportar falha.
TCP keepalive substitui timeout de leitura ou deadline?
Não. Um peer pode responder às sondas TCP e manter uma operação travada. Use deadline para o trabalho da aplicação e keepalive como proteção complementar.
SO_KEEPALIVE é suficiente no Linux?
Ele ativa o mecanismo, mas os padrões globais podem ser longos demais. Quando a aplicação exige detecção previsível, avalie TCP_KEEPIDLE, TCP_KEEPINTVL e TCP_KEEPCNT por socket.
HTTP keep-alive e TCP keepalive são iguais?
Não. HTTP keep-alive reutiliza conexões para requests; TCP keepalive sonda uma conexão ociosa no kernel. São camadas e objetivos diferentes.
Keepalive impede o proxy de fechar a conexão?
Não necessariamente. Teste o proxy real e conheça seu idle timeout. Para sessões persistentes, heartbeat no protocolo costuma ser mais explícito.
Como testar sem esperar horas?
Use parâmetros curtos apenas no ambiente de teste, simule descarte silencioso com rede isolada e observe o socket com ss e tcpdump. Restaurar a rede e limpar regras faz parte do teste.