The Problem – Sending Headers and Body Separately
Consider a web server responding to an HTTP request. The response has two parts: the HTTP headers (small, a few hundred bytes) and the page body (potentially large). A typical server writes them separately:
/* Naive approach – sends headers and body in separate TCP segments */
write(sockfd, http_headers, headers_len); /* sends one small TCP segment */
sendfile(sockfd, file_fd, NULL, file_size); /* sends body in another segment */
The problem here is that TCP will transmit the headers as a small, separate segment long before the file body is ready. This leads to:
| Problem | Why It Happens | Impact |
|---|---|---|
| Two TCP segments for one logical response | write() flushes the headers immediately before sendfile() is called | Wasted bandwidth on two sets of TCP/IP headers |
| Extra work for receiver | TCP stack on receiver must reassemble two segments | Slightly higher CPU usage on both ends |
| Inefficient for small responses | Headers + body often fit in one TCP segment (1460 bytes MSS on Ethernet) | One segment would have been enough |
The solution is TCP_CORK: cork the socket before writing, fill it with all the data you want to bundle, then uncork it to flush everything in one efficient segment.
What is TCP_CORK?
TCP_CORK is a Linux-specific socket option that tells the TCP stack to hold all outgoing data in an internal buffer and not transmit it until one of these conditions is met:
| # | Condition that causes a flush | Notes |
|---|---|---|
| 1 | TCP_CORK option is disabled (uncorked) | The most common and intended way to flush |
| 2 | Buffer reaches the maximum segment size (MSS) | Typically 1460 bytes on Ethernet |
| 3 | 200 ms timeout expires | Safety net in case the application forgets to uncork |
| 4 | Socket is closed | Remaining corked data is flushed on close() |
Think of TCP_CORK like putting a cork in a bottle. You fill the bottle with everything you want to send, then pull the cork out – and it all goes at once.
| Enable TCP_CORK setsockopt(CORK=1) |
→ | write() headers buffered, not sent |
→ | sendfile() body buffered, not sent |
→ | Disable TCP_CORK FLUSH – one segment |
How to Use TCP_CORK – setsockopt()
TCP_CORK is controlled using the setsockopt() system call. You enable it by setting the option value to 1 and disable it by setting it to 0.
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
int optval;
/* ---- Enable TCP_CORK ---- */
optval = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));
/*
* Now write headers and body – data is held in the kernel
* buffer and NOT transmitted yet.
*/
write(sockfd, http_headers, headers_len);
sendfile(sockfd, file_fd, NULL, file_size);
/* ---- Disable TCP_CORK – all buffered data is now flushed ---- */
optval = 0;
setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));
IPPROTO_TCP, not SOL_SOCKET. TCP_CORK is a TCP-level option, not a generic socket option.Complete HTTP File Server Example
Below is a realistic example of an HTTP server handler that uses TCP_CORK together with sendfile() to deliver headers and file body in a single TCP segment.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/sendfile.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
/*
* serve_file - sends an HTTP/1.0 response for a file request
* sockfd : connected client socket
* filepath : path to file to serve
*
* Strategy:
* 1. Cork the socket
* 2. Write HTTP headers with write()
* 3. Send file body with sendfile()
* 4. Uncork – headers + body sent together in as few segments as possible
*/
int serve_file(int sockfd, const char *filepath)
{
int file_fd;
struct stat st;
int optval;
char headers[512];
int headers_len;
off_t offset;
ssize_t sent;
size_t remaining;
/* Open the requested file */
file_fd = open(filepath, O_RDONLY);
if (file_fd == -1) {
/* Send 404 response */
const char *not_found =
"HTTP/1.0 404 Not Found\r\n"
"Content-Length: 0\r\n\r\n";
write(sockfd, not_found, strlen(not_found));
return -1;
}
if (fstat(file_fd, &st) == -1) {
perror("fstat");
close(file_fd);
return -1;
}
/* Build HTTP 200 response headers */
headers_len = snprintf(headers, sizeof(headers),
"HTTP/1.0 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %ld\r\n"
"\r\n",
(long)st.st_size);
/* Step 1: Cork the socket – data will be held until we uncork */
optval = 1;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval)) == -1) {
perror("setsockopt TCP_CORK enable");
/* Fall through – still works, just less efficient */
}
/* Step 2: Write HTTP headers – buffered, NOT sent yet */
if (write(sockfd, headers, headers_len) == -1) {
perror("write headers");
close(file_fd);
return -1;
}
/* Step 3: Send file body with sendfile() – still buffered by TCP_CORK */
offset = 0;
remaining = (size_t)st.st_size;
while (remaining > 0) {
sent = sendfile(sockfd, file_fd, &offset, remaining);
if (sent == -1) {
perror("sendfile");
close(file_fd);
return -1;
}
remaining -= (size_t)sent;
}
/* Step 4: Uncork – headers + file body are flushed as one TCP segment
* (or as few segments as the MSS allows) */
optval = 0;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval)) == -1) {
perror("setsockopt TCP_CORK disable");
}
close(file_fd);
return 0;
}
Checking the TCP_CORK state
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
/* Read back the current TCP_CORK state for a socket */
void print_cork_state(int sockfd)
{
int optval;
socklen_t optlen = sizeof(optval);
if (getsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, &optlen) == 0)
printf("TCP_CORK is %s\n", optval ? "ENABLED" : "DISABLED");
else
perror("getsockopt TCP_CORK");
}
The 200ms Timeout – Safety Net Explained
When TCP_CORK is enabled and you write data, the kernel starts a 200-millisecond timer. If you do not disable TCP_CORK within that window, the kernel automatically flushes whatever is in the buffer. This prevents data from being stuck forever in case your program:
Bug in application code skips the setsockopt(CORK=0) call
Process exits or is killed after corking but before sending all data
Slow file I/O or logic delays trigger the timeout
Interview Questions & Answers
When an application sends data in multiple write() or sendfile() calls, TCP may transmit each call as a separate segment. This is wasteful when the data logically belongs together (like HTTP headers and a response body) and could fit into fewer segments. TCP_CORK buffers outgoing data in the kernel until the application explicitly flushes it by disabling the option, resulting in fewer, larger, more efficient TCP segments.
Using setsockopt() with level IPPROTO_TCP and option name TCP_CORK. Set optval to 1 to enable (cork the socket) and to 0 to disable (uncork and flush). It is a Linux-specific option defined in netinet/tcp.h.
Four things: (1) you disable TCP_CORK with setsockopt(), (2) the buffered data reaches the maximum segment size (MSS), (3) the socket is closed, or (4) 200 milliseconds elapse after the first corked byte is written. The 200ms timeout is a safety mechanism so data is never permanently stuck in the buffer.
Nagle’s algorithm (enabled by default on TCP sockets) delays small packets until the previous unacknowledged packet is acknowledged, reducing the number of small packets at the cost of latency. TCP_CORK is different – it buffers data regardless of acknowledgements and waits for the application to explicitly release it or for the 200ms timer. TCP_CORK is useful when you know you will have all the data ready in one burst (like headers+body), whereas Nagle helps in interactive protocols where writes are unpredictably small and frequent. You can have both enabled at the same time.
No. TCP_CORK is Linux-specific. FreeBSD has an equivalent called TCP_NOPUSH, which provides similar functionality. macOS inherits TCP_NOPUSH from FreeBSD. If you are writing cross-platform code, you need to use preprocessor conditionals to choose between TCP_CORK (Linux) and TCP_NOPUSH (BSD/macOS).
The kernel will automatically flush the corked data after 200 milliseconds. However, this adds unwanted latency to the response. If the application closes the socket without uncorking, the remaining corked data is flushed as part of the close operation.
Next: MSG_MORE – Per-Call Output Control
Compare TCP_CORK with MSG_MORE and learn when each approach is more appropriate.
