---
title: "TCP_NODELAY em Zig: Nagle, REUSEADDR e Opções de Socket"
url: "https://ziglang.com.br/artigos/zig-tcp-nodelay-nagle-reuseaddr-socket-options/"
markdown_url: "https://ziglang.com.br/artigos/zig-tcp-nodelay-nagle-reuseaddr-socket-options.MD"
description: "Guia prático de opções TCP em Zig: TCP_NODELAY, algoritmo de Nagle, SO_REUSEADDR, buffers, linger, keepalive, latência, testes e produção."
date: "2026-08-28"
author: ""
---

# TCP_NODELAY em Zig: Nagle, REUSEADDR e Opções de Socket

Guia prático de opções TCP em Zig: TCP_NODELAY, algoritmo de Nagle, SO_REUSEADDR, buffers, linger, keepalive, latência, testes e produção.


Para reduzir a latência de mensagens TCP pequenas em Zig, você pode habilitar **`TCP_NODELAY`** no socket com `std.posix.setsockopt`, desativando o algoritmo de Nagle naquela conexão. Mas essa opção não é um botão universal de “mais performance”: ela troca eficiência de pacotes por menor espera em certos padrões interativos. Para servidores reiniciáveis, `SO_REUSEADDR` resolve outro problema; para throughput, `SO_SNDBUF` e `SO_RCVBUF` tratam filas do kernel; para detectar peers desaparecidos, a ferramenta é keepalive. Cada opção responde a uma pergunta diferente.

A melhor política é configurar sockets em um módulo pequeno, documentar o motivo de cada valor e medir o efeito no tráfego real. Uma API de chat, um banco de dados, um jogo e um download de arquivo usam TCP, mas não têm o mesmo tamanho de mensagem, orçamento de latência ou comportamento sob pressão.

Este guia complementa o tutorial de [sockets TCP e UDP em Zig](/artigos/zig-networking-sockets-tcp-udp/), o artigo sobre [TCP keepalive e conexões mortas](/artigos/zig-tcp-keepalive-detectar-conexoes-mortas/) e o padrão de [graceful shutdown com SIGTERM](/artigos/zig-graceful-shutdown-sigterm-sigint/). Como Zig ainda evolui antes do 1.0, confirme nomes, tipos e constantes de `std.posix` na versão fixada pelo projeto.

## Resposta rápida

| Objetivo | Opção ou técnica | Cuidado principal |
|---|---|---|
| reduzir atraso de mensagens pequenas | `TCP_NODELAY` | pode aumentar a quantidade de pacotes |
| reiniciar listener sem esperar conexões antigas | `SO_REUSEADDR` | semântica varia entre sistemas |
| múltiplos listeners na mesma porta | `SO_REUSEPORT` | distribuição e segurança exigem projeto explícito |
| detectar peer desaparecido | `SO_KEEPALIVE` + opções TCP | não substitui deadline da aplicação |
| aumentar fila de envio | `SO_SNDBUF` | mais memória e possível bufferbloat |
| aumentar fila de recepção | `SO_RCVBUF` | pode esconder consumidor lento |
| limitar bloqueio de I/O | timeout/deadline | APIs variam por plataforma |
| definir fechamento abortivo | `SO_LINGER` | configuração errada perde dados |
| reduzir syscalls e pacotes | batching na aplicação | acrescenta espera controlada |
| validar melhoria | benchmark + captura de pacotes | média sozinha esconde caudas |

## O que é o algoritmo de Nagle

Uma aplicação pode chamar `write` várias vezes com poucos bytes:

```text
write("O")
write("K")
write("\n")
```

Se cada chamada virasse imediatamente um segmento TCP, a rede carregaria muitos pacotes cujo cabeçalho é maior que o payload. O algoritmo de Nagle reduz esse desperdício. Em termos simplificados, quando já existem dados enviados e ainda não confirmados, o kernel pode aguardar para combinar novas escritas pequenas num segmento maior.

Isso costuma ser bom para eficiência. O problema aparece em protocolos do tipo pergunta-resposta com mensagens curtas. A aplicação envia alguns bytes, espera a resposta, o peer também envia poucos bytes e os mecanismos de agrupamento e confirmação podem introduzir uma espera perceptível. A interação entre Nagle, delayed ACK, buffering da aplicação e agenda do sistema operacional é mais importante que a opção isolada.

`TCP_NODELAY` diz ao kernel para não aplicar Nagle naquele socket. O nome parece significar “TCP sem qualquer atraso”, mas não é isso. A conexão ainda pode atrasar por:

- DNS e handshake;
- congestionamento ou perda de pacotes;
- escalonamento de threads;
- buffering na aplicação;
- proxy ou load balancer;
- fila cheia no kernel;
- coleta ou processamento do peer;
- retransmissão TCP;
- algoritmo do próprio protocolo.

Portanto, ative `TCP_NODELAY` para um **padrão de mensagens**, não para compensar uma arquitetura que nunca foi medida.

## Quando TCP_NODELAY faz sentido

Bons candidatos incluem:

- terminal remoto ou shell interativo;
- jogo com comandos pequenos e frequentes;
- RPC com requests e respostas curtos;
- protocolo de controle entre agente e servidor;
- conexão com banco que alterna frames pequenos;
- WebSocket interativo com baixa tolerância a atraso;
- replicação em que cada confirmação libera a próxima etapa.

Casos em que o ganho tende a ser menor:

- upload ou download de arquivos grandes;
- streaming com chunks naturalmente grandes;
- replicação em lotes;
- envio de logs acumulados;
- resposta HTTP já montada num buffer razoável;
- workload limitado por CPU, disco ou banco de dados.

A pergunta útil não é “`TCP_NODELAY` é mais rápido?”, mas: **o tempo de espera causado pelo agrupamento de segmentos é relevante para o percentil de latência que importa ao produto?**

## Configurando TCP_NODELAY em Zig

Opções específicas do socket normalmente passam por `std.posix.setsockopt`. Um helper conceitual para valores inteiros pode ser escrito assim:

```zig
const std = @import("std");

fn setIntSocketOption(
    fd: std.posix.socket_t,
    level: u32,
    option: u32,
    enabled: bool,
) !void {
    const value: c_int = if (enabled) 1 else 0;
    try std.posix.setsockopt(
        fd,
        level,
        option,
        std.mem.asBytes(&value),
    );
}

fn enableTcpNoDelay(stream: std.net.Stream) !void {
    try setIntSocketOption(
        stream.handle,
        std.posix.IPPROTO.TCP,
        std.posix.TCP.NODELAY,
        true,
    );
}
```

Dependendo da versão de Zig, as constantes podem estar organizadas de outra forma e o tipo aceito por `setsockopt` pode mudar. Procure na stdlib instalada em vez de copiar cegamente um snippet:

```bash
zig env
rg "pub fn setsockopt|NODELAY|REUSEADDR|SNDBUF|RCVBUF" /caminho/da/lib/std
```

Mantenha a variação de plataforma confinada. O restante da aplicação deveria chamar uma função como `configureInteractiveTcp(stream)` e não repetir níveis, constantes e conversões em cada handler.

## Aplicando no cliente e no servidor

No cliente, configure logo depois de conectar:

```zig
const address = try std.net.Address.parseIp("192.0.2.10", 9000);
const stream = try std.net.tcpConnectToAddress(address);
errdefer stream.close();

try enableTcpNoDelay(stream);

try stream.writeAll("PING\n");
```

No servidor, configure a conexão aceita, não apenas o listener:

```zig
while (true) {
    const connection = try server.accept();

    enableTcpNoDelay(connection.stream) catch |err| {
        std.log.err("tcp_nodelay_failed error={}", .{err});
        connection.stream.close();
        continue;
    };

    // Entregue o stream configurado ao handler ou ao pool.
}
```

Algumas opções pertencem ao listener; outras, a cada conexão. `TCP_NODELAY` é uma propriedade do socket TCP conectado. Se você configurar apenas o socket de escuta e presumir herança sem validar a plataforma, pode colocar em produção uma política diferente da esperada.

Uma fábrica de conexões é o lugar mais seguro para centralizar isso:

```zig
const TcpProfile = enum {
    interactive,
    bulk_transfer,
    internal_rpc,
};

fn configureTcp(stream: std.net.Stream, profile: TcpProfile) !void {
    switch (profile) {
        .interactive, .internal_rpc => try enableTcpNoDelay(stream),
        .bulk_transfer => {},
    }
}
```

O enum torna a intenção revisável. Um número solto em `main.zig` não explica por que a opção existe.

## TCP_NODELAY não substitui batching

Desabilitar Nagle não transforma três `write` minúsculos numa boa estratégia. Se os bytes já estão disponíveis juntos, envie-os juntos:

```zig
var buffer: [128]u8 = undefined;
const message = try std.fmt.bufPrint(
    &buffer,
    "{s} {d}\n",
    .{ "STATUS", request_id },
);
try stream.writeAll(message);
```

Isso reduz syscalls, simplifica captura de pacotes e mantém a mensagem coerente. `TCP_NODELAY` é útil quando você **não quer que o kernel espere por dados futuros**; batching é útil quando a aplicação **já conhece os dados agora**.

Para protocolos com alto volume, use uma fila por conexão e um limite pequeno de batching, por quantidade de bytes ou por janela de microssegundos. O limite é essencial: lote sem prazo vira latência; envio imediato sem lote vira tempestade de syscalls.

## SO_REUSEADDR: reinício previsível do servidor

Depois que uma conexão TCP fecha, estados como `TIME_WAIT` continuam existindo por algum tempo. Isso protege a rede contra segmentos atrasados de uma conexão antiga. Durante deploys rápidos, um novo processo pode tentar abrir o mesmo endereço e receber `AddressInUse`.

`SO_REUSEADDR` costuma permitir que o listener seja recriado sem esperar todo o estado antigo desaparecer. Configure antes do `bind`/`listen`. O desenho conceitual em API POSIX é:

```zig
const fd = try std.posix.socket(
    std.posix.AF.INET,
    std.posix.SOCK.STREAM,
    std.posix.IPPROTO.TCP,
);
errdefer std.posix.close(fd);

try setIntSocketOption(
    fd,
    std.posix.SOL.SOCKET,
    std.posix.SO.REUSEADDR,
    true,
);

// bind(fd, ...), listen(fd, ...)
```

As abstrações de `std.net.Address.listen` podem oferecer uma opção equivalente na struct de configuração da sua versão. Prefira a API de alto nível quando ela expressar exatamente a política necessária; desça para `std.posix` quando precisar controlar a ordem ou uma opção não exposta.

`SO_REUSEADDR` não significa “qualquer processo pode compartilhar esta porta”. O comportamento exato difere entre Linux, BSD, macOS e Windows. Faça teste de integração no target de produção, inclusive com um listener antigo, conexões em `TIME_WAIT` e tentativa de segundo bind.

## SO_REUSEPORT não é sinônimo de REUSEADDR

`SO_REUSEPORT` permite, em plataformas que suportam a opção, mais de um socket vinculado ao mesmo endereço e porta sob regras específicas. O kernel pode distribuir conexões entre listeners. Isso pode ser útil para:

- um listener por thread ou processo;
- rolling restart com sobreposição controlada;
- redução de contenção em um accept loop central.

Mas ele também muda o modelo operacional. Você precisa saber:

- como o kernel distribui conexões;
- se todos os processos têm a mesma configuração e versão;
- como readiness remove uma réplica;
- o que acontece durante deploy parcial;
- quais usuários podem abrir socket compatível;
- se métricas agregam todos os listeners.

Não ative `SO_REUSEPORT` só porque o nome parece uma versão melhor de `SO_REUSEADDR`. Em muitos serviços, um listener e um pool de workers são mais simples e suficientemente rápidos.

## Buffers de envio e recepção

`SO_SNDBUF` controla a capacidade associada à fila de envio do socket. `SO_RCVBUF` faz o mesmo para a recepção. Os sistemas modernos aplicam autotuning, limites globais e, em alguns casos, multiplicam internamente o valor solicitado para metadados.

Buffers maiores podem ajudar quando o produto banda-latência é grande: muita largura de banda e RTT alto exigem dados suficientes em voo. Porém, aumentar buffers indiscriminadamente cria custos:

- mais memória por conexão;
- filas mais longas antes de a aplicação perceber saturação;
- maior latência de cauda;
- pressão de memória com milhares de clientes;
- sensação falsa de que backpressure foi resolvido.

Imagine 20 mil conexões com buffers efetivos de centenas de kilobytes. Mesmo que poucas encham ao mesmo tempo, a capacidade potencial importa. Meça RSS, memória de socket no host e quantidade de conexões, não apenas throughput.

No Linux, `ss` ajuda a observar filas:

```bash
ss -tinm '( sport = :9000 or dport = :9000 )'
```

Para parâmetros globais:

```bash
sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
```

Não altere `sysctl` como primeira resposta. Descubra primeiro se o socket realmente está limitado, se há perda, se o consumidor é lento ou se o protocolo produz mensagens pequenas demais.

## Timeouts, keepalive e deadlines

Opções de performance não substituem limites de tempo. Um perfil TCP de produção normalmente combina:

1. **connect timeout** para DNS e handshake;
2. **deadline da operação** para o request completo;
3. **read/write timeout** para I/O bloqueado;
4. **idle timeout** para sessão sem atividade útil;
5. **TCP keepalive** para peer que desapareceu silenciosamente;
6. **`TCP_NODELAY`**, quando mensagens pequenas são sensíveis à latência;
7. **backpressure** para consumidor lento;
8. **retry com backoff**, apenas quando a operação permite.

O guia de [timeout, retry e circuit breaker em Zig](/artigos/zig-circuit-breaker-timeout-retry/) detalha a parte de resiliência. O ponto central é não confundir “pacote saiu sem esperar Nagle” com “operação possui prazo”.

## SO_LINGER: use com muito cuidado

`SO_LINGER` controla parte do comportamento de `close` quando ainda existem dados pendentes. Uma configuração abortiva pode fazer o fechamento gerar `RST`, descartando dados não enviados. Isso é útil em casos raros e perigoso como padrão.

Erros comuns:

- ativar linger para “fechar mais rápido” e perder a resposta final;
- bloquear o encerramento esperando dados além do prazo do processo;
- confundir `close` bem-sucedido localmente com entrega confirmada pela aplicação remota;
- usar RST para esconder protocolo de shutdown mal definido.

Para a maioria dos servidores, prefira um encerramento de protocolo explícito, pare de aceitar trabalho novo, drene com deadline e então feche o socket. Integre esse fluxo ao [readiness/liveness do serviço Zig](/artigos/zig-health-check-readiness-liveness/).

## Um módulo de configuração por perfil

Uma aplicação madura pode representar a política sem expor detalhes POSIX ao domínio:

```zig
const TcpOptions = struct {
    no_delay: bool = false,
    keepalive: bool = true,
    send_buffer: ?u32 = null,
    receive_buffer: ?u32 = null,
};

fn interactiveProfile() TcpOptions {
    return .{
        .no_delay = true,
        .keepalive = true,
    };
}

fn bulkProfile() TcpOptions {
    return .{
        .no_delay = false,
        .keepalive = true,
    };
}
```

A função que aplica essas opções deve:

- ser condicionada por plataforma;
- retornar erros, não ignorá-los;
- registrar a política uma vez na inicialização;
- permitir teste unitário da seleção de perfil;
- permitir teste de integração do socket real;
- evitar valores globais mutáveis;
- documentar unidades e defaults.

Se uma opção é desejável, mas não obrigatória em todos os targets, diferencie “não suportada” de “falhou inesperadamente”. Uma CLI multiplataforma pode continuar sem uma otimização. Um serviço Linux de baixa latência talvez deva falhar no startup se a política essencial não foi aplicada.

## Como medir sem se enganar

Faça benchmark com duas configurações idênticas, mudando uma opção por vez. Colete pelo menos:

- latência p50, p95, p99 e máxima;
- requests ou mensagens por segundo;
- uso de CPU do cliente e servidor;
- syscalls por operação;
- quantidade e tamanho dos pacotes;
- retransmissões;
- bytes em fila;
- conexões abertas e fechadas;
- memória por processo e memória de sockets.

Um teste simples de loopback pode ocultar o problema porque o RTT é quase zero e não há perda. Use também uma rede controlada com latência e jitter representativos. Em Linux, namespaces e `tc netem` ajudam:

```bash
sudo tc qdisc add dev eth0 root netem delay 30ms 5ms loss 0.1%
# execute o teste
sudo tc qdisc del dev eth0 root
```

Capture pacotes:

```bash
sudo tcpdump -i any -nn 'tcp port 9000' -w /tmp/zig-tcp-options.pcap
```

No Wireshark, compare quantidade de segmentos pequenos, intervalos entre request e resposta, retransmissões e encerramentos com RST. Para syscalls:

```bash
strace -f -c ./zig-out/bin/servidor
```

Se `TCP_NODELAY` reduz p99, mas dobra pacotes e CPU sem melhorar o objetivo do produto, talvez batching explícito seja a solução melhor.

## Armadilhas frequentes

### Ativar tudo por padrão

Copiar uma lista de opções de um servidor famoso não reproduz o workload dele. Cada flag precisa de hipótese e métrica.

### Otimizar a média

Uma média de 3 ms pode esconder p99 de 200 ms. Protocolos interativos vivem na cauda.

### Confundir writeAll com entrega remota

`writeAll` entrega os bytes ao caminho local de I/O ou retorna erro. Não prova que a aplicação remota processou a mensagem. Se você precisa dessa garantia, crie ACK no protocolo.

### Usar buffers maiores para esconder fila infinita

Fila cheia deve produzir backpressure observável: pausar leitura, rejeitar, descartar conforme política ou desconectar cliente lento. Memória não é estratégia de controle de carga.

### Ignorar proxies

Se existe Nginx ou load balancer, parte da conexão termina nele. A opção aplicada entre Zig e o proxy não altera diretamente o socket entre proxy e cliente. Entenda cada perna da rede. O guia de [Zig atrás de Nginx](/artigos/zig-nginx-proxy-reverso-load-balancing/) mostra esse contrato.

### Não testar o restart

`SO_REUSEADDR` só prova valor quando você mata, reinicia e observa estados reais. Inclua isso no teste de deploy.

## Checklist de produção

Antes de publicar um serviço TCP em Zig:

- [ ] o perfil da conexão está documentado como interativo, RPC ou bulk;
- [ ] `TCP_NODELAY` tem hipótese mensurável;
- [ ] mensagens disponíveis juntas são enviadas em lote;
- [ ] `SO_REUSEADDR` é configurado antes do bind, quando necessário;
- [ ] `SO_REUSEPORT` não foi ativado sem desenho multiprocesso;
- [ ] buffers usam defaults ou valores justificados por medição;
- [ ] há deadline da aplicação além de opções TCP;
- [ ] keepalive e heartbeat têm papéis separados;
- [ ] consumidor lento encontra limite e backpressure;
- [ ] shutdown drena conexões com prazo;
- [ ] benchmark mede percentis, CPU, pacotes e retransmissões;
- [ ] teste roda no mesmo sistema operacional de produção;
- [ ] falha ao aplicar opção essencial não é ignorada;
- [ ] métricas identificam o perfil e a versão do binário.

## Perguntas frequentes

### Quando devo ativar TCP_NODELAY em Zig?

Quando mensagens pequenas e interativas sofrem com espera de agrupamento: RPC curto, terminal, jogo, controle ou WebSocket sensível à latência. Meça antes e depois; transferências grandes raramente ganham pelo mesmo motivo.

### O algoritmo de Nagle sempre aumenta a latência?

Não. Ele pode não ter efeito perceptível quando a aplicação envia buffers grandes, quando não há dados pendentes ou quando outros custos dominam. O atraso depende do padrão de escrita e confirmação.

### SO_REUSEADDR permite dois servidores na mesma porta?

Não como regra geral. Ele é usado principalmente para facilitar rebinding após restart. Compartilhamento intencional costuma envolver `SO_REUSEPORT`, cuja semântica varia e exige projeto específico.

### Buffers maiores deixam TCP mais rápido?

Somente quando o tamanho atual limita uma conexão que precisa manter mais dados em voo. Em outros casos, buffers maiores apenas consomem memória e alongam filas.

### TCP_NODELAY substitui flush?

Não. Flush atua no buffer da biblioteca ou da aplicação. `TCP_NODELAY` atua no agrupamento TCP feito pelo kernel. Você pode precisar de ambos, de um ou de nenhum.

### Como confirmar que a opção foi aplicada?

Além de tratar o retorno de `setsockopt`, consulte o socket com `getsockopt` quando a API da versão permitir e faça um teste de integração. Depois valide o efeito por captura de pacotes, não apenas por log de startup.

## Próximos passos

Se você ainda está montando o protocolo, comece por [networking com TCP e UDP em Zig](/artigos/zig-networking-sockets-tcp-udp/). Para conexões longas, adicione [TCP keepalive](/artigos/zig-tcp-keepalive-detectar-conexoes-mortas/). Se cliente e servidor rodam no mesmo host, compare com [Unix domain sockets](/artigos/zig-unix-domain-sockets-ipc-nginx-systemd/). Em serviços web, alinhe as opções com [Nginx, timeouts e load balancing](/artigos/zig-nginx-proxy-reverso-load-balancing/) e finalize o ciclo com [observabilidade de logs e métricas Prometheus](/artigos/zig-observabilidade-logs-prometheus/).

A regra prática é simples: **use `TCP_NODELAY` para latência pequena e medida, `SO_REUSEADDR` para ciclo de vida do listener, buffers para uma limitação demonstrada e deadlines para controlar o tempo da operação**. Opções de socket são ferramentas cirúrgicas. Em Zig, vale preservar essa clareza no código em vez de transformar o startup numa coleção de flags sem contexto.
