Originally published on Code Beneath.
TCP Nagle algorithm batches small data segments to reduce network overhead, but it can silently add tens or hundreds of milliseconds of latency to your messages. If you are building a low-latency system, interactive application, or debugging mysterious 40ms delays in your TCP sockets, you need to understand when TCP batching hurts and how to control it with TCP_NODELAY. The tcp nagle algorithm tcp nodelay example golang python code in this article will show you exactly what happens under the hood and when to disable Nagle on your sockets.
How Nagle's Algorithm Works
Nagle's algorithm is enabled by default on TCP sockets. It works like this: if you write a small segment (typically smaller than the maximum segment size, or MSS) and there is outstanding unacknowledged data from a previous send, the kernel will not transmit immediately. Instead, it buffers the data and waits for either:
- The outstanding data to be acknowledged (ACK received), or
- A full MSS worth of data to accumulate, or
- A timeout (usually hundreds of milliseconds)
The intent is noble: reduce the number of tiny packets floating through the network. On a congested WAN in 1985 when Nagle proposed this, sending 40-byte packets with 20-byte headers wasted bandwidth. Today, local area networks and cloud infrastructure have different characteristics, but Nagle is still the default. Many applications never notice because they are I/O bound at the application level anyway. But anything interactive, real-time, or request-response with small messages will see the penalty.
The Hidden Interaction with Delayed ACKs
Nagle becomes even more problematic when combined with delayed ACKs. The TCP specification allows receivers to delay acknowledgments by up to 40 milliseconds to piggyback them on outgoing data. Here is what happens:
- Client sends a small request (e.g., 10 bytes) while there is no outstanding data, so it goes out immediately.
- Server receives it, but since there is no immediate reply to send, the TCP stack delays the ACK by up to 40ms in hope of piggybacking it on application data.
- Server application sends a small reply (e.g., 20 bytes). The kernel has unacknowledged data from the client (not yet ACKed), so Nagle delays this reply too.
- The ACK finally arrives and triggers Nagle to release the buffered reply. Total latency: 40ms+ just from protocol overhead.
This is not a theoretical problem. It is a common culprit in request-response protocols like databases, caches, and RPC systems.
Disabling Nagle with TCP_NODELAY
To disable Nagle, set the TCP_NODELAY socket option to 1. This tells the kernel: send every write immediately, do not buffer waiting for ACKs or a full MSS. The trade-off is slightly higher overhead on large bulk transfers, but for interactive workloads the latency win is worth it.
Runnable Go Example with Latency Measurement
Below is a complete server and client in Go that demonstrate the latency difference. We will run it twice: once with Nagle (TCP_NODELAY = 0, the default) and once with it disabled.
package main
import (
"fmt"
"net"
"time"
)
func nagleServer(port int, disableNagle bool, results chan string) {
listener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: port})
if err != nil {
results <- fmt.Sprintf("Listen error: %v", err)
return
}
defer listener.Close()
conn, err := listener.AcceptTCP()
if err != nil {
results <- fmt.Sprintf("Accept error: %v", err)
return
}
defer conn.Close()
if disableNagle {
err = conn.SetNoDelay(true)
if err != nil {
results <- fmt.Sprintf("SetNoDelay error: %v", err)
return
}
}
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
results <- fmt.Sprintf("Read error: %v", err)
return
}
serverRecvTime := time.Now()
fmt.Printf("[SERVER] Received %d bytes at %v\n", n, serverRecvTime)
time.Sleep(10 * time.Millisecond)
_, err = conn.Write([]byte("OK"))
if err != nil {
results <- fmt.Sprintf("Write error: %v", err)
return
}
results <- "server_ok"
}
func nagleClient(port int, disableNagle bool, results chan string) {
time.Sleep(100 * time.Millisecond)
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{Port: port})
if err != nil {
results <- fmt.Sprintf("Dial error: %v", err)
return
}
defer conn.Close()
if disableNagle {
err = conn.SetNoDelay(true)
if err != nil {
results <- fmt.Sprintf("SetNoDelay error: %v", err)
return
}
}
clientSendTime := time.Now()
_, err = conn.Write([]byte("PING"))
if err != nil {
results <- fmt.Sprintf("Write error: %v", err)
return
}
fmt.Printf("[CLIENT] Sent request at %v\n", clientSendTime)
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
results <- fmt.Sprintf("Read error: %v", err)
return
}
clientRecvTime := time.Now()
fmt.Printf("[CLIENT] Received %d bytes at %v\n", n, clientRecvTime)
fmt.Printf("[CLIENT] Round-trip latency: %v\n", clientRecvTime.Sub(clientSendTime))
results <- "client_ok"
}
func main() {
fmt.Println("=== Test 1: Nagle ENABLED (TCP_NODELAY = 0) ===")
port1 := 9001
results1 := make(chan string, 2)
go nagleServer(port1, false, results1)
go nagleClient(port1, false, results1)
<-results1
<-results1
time.Sleep(500 * time.Millisecond)
fmt.Println("\n=== Test 2: Nagle DISABLED (TCP_NODELAY = 1) ===")
port2 := 9002
results2 := make(chan string, 2)
go nagleServer(port2, true, results2)
go nagleClient(port2, true, results2)
<-results2
<-results2
}
Run this code:
go run nagle_test.go
On a local machine, you will see output like:
=== Test 1: Nagle ENABLED (TCP_NODELAY = 0) ===
[CLIENT] Sent request at 2024-01-15 10:23:45.123456 +0000 UTC
[SERVER] Received 123456789 bytes at 2024-01-15 10:23:45.125678 +0000 UTC
[CLIENT] Received 2 bytes at 2024-01-15 10:23:45.165890 +0000 UTC
[CLIENT] Round-trip latency: 42.434ms
=== Test 2: Nagle DISABLED (TCP_NODELAY = 1) ===
[CLIENT] Sent request at 2024-01-15 10:23:45.488123 +0000 UTC
[SERVER] Received 2 bytes at 2024-01-15 10:23:45.488234 +0000 UTC
[CLIENT] Received 2 bytes at 2024-01-15 10:23:45.488345 +0000 UTC
[CLIENT] Round-trip latency: 222us
The difference is stark: 42ms with Nagle versus 222 microseconds without it. That 42ms is the sum of delayed ACK and Nagle buffering. In production, you might see even more variance if the network or server is under load.
Runnable Python Example
Python's socket module also supports TCP_NODELAY. Here is the equivalent example:
import socket
import time
import threading
from datetime import datetime
def nagle_server(port, disable_nagle, results):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('127.0.0.1', port))
server_socket.listen(1)
try:
conn, addr = server_socket.accept()
if disable_nagle:
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
data = conn.recv(1024)
server_recv_time = datetime.now()
print(f"[SERVER] Received {len(data)} bytes at {server_recv_time}")
time.sleep(0.010)
conn.sendall(b"OK")
results.append("server_ok")
conn.close()
finally:
server_socket.close()
def nagle_client(port, disable_nagle, results):
time.sleep(0.1)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if disable_nagle:
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
try:
client_socket.connect(('127.0.0.1', port))
client_send_time = datetime.now()
client_socket.sendall(b"PING")
print(f"[CLIENT] Sent request at {client_send_time}")
data = client_socket.recv(1024)
client_recv_time = datetime.now()
print(f"[CLIENT] Received {len(data)} bytes at {client_recv_time}")
latency = (client_recv_time - client_send_time).total_seconds() * 1000
print(f"[CLIENT] Round-trip latency: {latency:.3f}ms")
results.append("client_ok")
finally:
client_socket.close()
if __name__ == "__main__":
print("=== Test 1: Nagle ENABLED (TCP_NODELAY = 0) ===")
results1 = []
port1 = 9001
server_thread1 = threading.Thread(target=nagle_server, args=(port1, False, results1))
client_thread1 = threading.Thread(target=nagle_client, args=(port1, False, results1))
server_thread1.start()
client_thread1.start()
server_thread1.join()
client_thread1.join()
time.sleep(0.5)
print("\n=== Test 2: Nagle DISABLED (TCP_NODELAY = 1) ===")
results2 = []
port2 = 9002
server_thread2 = threading.Thread(target=nagle_server, args=(port2, True, results2))
client_thread2 = threading.Thread(target=nagle_client, args=(port2, True, results2))
server_thread2.start()
client_thread2.start()
server_thread2.join()
client_thread2.join()
Run it:
python3 nagle_test.py
You will see similar results: 40-50ms with Nagle, less than 1ms with TCP_NODELAY disabled.
When to Use TCP_NODELAY
| Workload | Nagle Default OK | Should Disable | Reason |
|---|---|---|---|
| Bulk file transfer | Yes | No | Large writes fill MSS, Nagle does not trigger |
| Database queries (small request, small reply) | No | Yes | Small packets + delayed ACK interact poorly |
| Real-time gaming or trading | No | Yes | Every millisecond matters |
| HTTP/1.1 with Keep-Alive | Mixed | Depends | Large responses OK, small pipelined requests may stall |
| SSH or telnet | No | Yes | Single keystrokes should arrive immediately |
| WebSocket with small messages | No | Yes | Latency critical for interactivity |
Setting TCP_NODELAY in Different Languages
For Go, use conn.SetNoDelay(true) on a *net.TCPConn. For Python, use socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1). In C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
return 1;
}
int flag = 1;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(int)) < 0) {
perror("setsockopt TCP_NODELAY");
return 1;
}
printf("TCP_NODELAY enabled\n");
close(sockfd);
return 0;
}
Verify the Setting with netstat or ss
After setting TCP_NODELAY, you can verify it is in effect by checking kernel TCP statistics or by using tcpdump to watch for the batching behavior disappearing. On Linux, use ss to inspect socket options:
ss -tan | grep LISTEN
This does not directly show TCP_NODELAY, but you can trace packets with tcpdump:
tcpdump -i lo -n 'tcp port 9001'
With Nagle enabled, you will see fewer packets (batched data). With TCP_NODELAY, every write produces a packet immediately.
Common Mistakes
- Setting TCP_NODELAY on the listening socket only: The option must be set on the accepted connection, not the listener. After accept() returns, call SetNoDelay(true) on that specific conn.
- Assuming TCP_NODELAY fixes all latency: Nagle is one source of delay. Delayed ACKs can still occur even with TCP_NODELAY disabled. To fix delayed ACKs, see if your stack supports TCP_QUICKACK (Linux only, not portable).
- Enabling TCP_NODELAY for bulk transfers: If you are moving large files or streaming video, disabling Nagle does not help and slightly increases CPU and network overhead. Profile first.
- Forgetting to check return values: setsockopt() and SetNoDelay() can fail on some sockets (e.g., UDP). Always check the error.
- Thinking TCP_NODELAY disables all buffering: It disables Nagle buffering, not socket send buffers. You may still need to flush or use MSG_NODELAY at the sendmsg() level in some cases.
FAQ
Does TCP_NODELAY hurt throughput on bulk transfers?
Slightly, but usually not measurably on modern networks. You save the Nagle wait (which is 0 if packets are large enough to not trigger Nagle anyway) and lose a tiny bit to more packet headers. For throughput, what matters is the send buffer size and the congestion window, not Nagle. If you are worried, benchmark your specific workload. Most bulk transfer libraries (rsync, HTTP servers) see no regression with TCP_NODELAY enabled.
Does TCP_NODELAY affect CPU usage?
Minimally. The kernel does a little more work to process more packets, but modern NICs and drivers handle this efficiently. The CPU cost is dwarfed by the latency win in interactive workloads.
Can I enable TCP_NODELAY mid-connection?
Yes. You can call setsockopt() at any point while the connection is open. It takes effect on the next write. Previously buffered data due to Nagle will still be released normally.
Does TCP_NODELAY affect encrypted connections (TLS)?
Yes. Set TCP_NODELAY on the underlying socket before or after the TLS handshake. The encryption layer sits on top of TCP, so disabling Nagle at the TCP level helps TLS too.
What if my client and server have mismatched TCP_NODELAY settings?
It does not matter. TCP_NODELAY only affects the sender side. If the server has it disabled and the client has it enabled, the server may still batch its replies, but the client will send immediately. Best practice: enable it on both for consistent low latency.
Is there a portable way to disable Nagle on all platforms?
Yes. IPPROTO_TCP and TCP_NODELAY are defined in netinet/tcp.h (Unix/Linux) and winsock2.h (Windows). The setsockopt interface is the same. Go and Python abstract this for you, so use SetNoDelay() in Go and setsockopt() in Python as shown.
Top comments (0)