TCP_CORK – Batching TCP Segments

 

TCP_CORK – Batching TCP Segments
Chapter 61 · Part 2 of 4 · Sockets: Advanced Topics
1
Segment Instead of 2
200ms
Auto-Flush Timeout
Linux
Specific Option

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));
Important: The level parameter is 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:

Forgets to uncork

Bug in application code skips the setsockopt(CORK=0) call

Crashes before uncorking

Process exits or is killed after corking but before sending all data

Takes too long between writes

Slow file I/O or logic delays trigger the timeout

Warning: Do not rely on the 200ms timeout as a normal mechanism. Always explicitly disable TCP_CORK after you have written all the data you want to batch. The timeout is only a safeguard against programming errors.

Interview Questions & Answers

Q1. What problem does TCP_CORK solve?

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.

Q2. How do you enable and disable TCP_CORK?

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.

Q3. What triggers a flush when TCP_CORK is enabled?

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.

Q4. What is the difference between TCP_CORK and Nagle’s algorithm?

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.

Q5. Is TCP_CORK available on FreeBSD or macOS?

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).

Q6. What happens if you forget to uncork after enabling TCP_CORK?

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.

Continue to Part 3 → Back to Index

Leave a Reply

Your email address will not be published. Required fields are marked *