Understanding SO_REUSEPORT and TCP_CORK Through a Coffee Shop Analogy

by gg582 · 2026-09-13 16:50:43 · 9 views

Table of contents

This post is not generated by AI. And I am not a native speaker of English. I haven't learned professional English education like many Korean developers do; I just live in a small South Korean city. Therefore, there are many grammar mistakes. I only used a grammar checker. Some sentences are unnatural.

How can we assume the problem?

Assume you should make eight cups of coffee.

You have single-serving Hario V60 drippers and 8-serving Hario V60 drippers.

When you try single-serving V60 drippers with eight baristas, it takes less than giving a dripper to a senior barista.

But each barista should prepare for each extraction when many orders are waiting per person since a dripper is small.

When 16 cups of coffee should be prepared, it can't be done so fast like we've already done.

When you use a small dripper eight times, you need more time. But when you use a big dripper, huge amounts are extracted for just one time.

There's an important point. When each barista just uses a bigger dripper, they can extract much more than using a small one.

But, when a dripper is too big, each customer should wait more to finish that extraction.

Hiring more baristas means SO_REUSEPORT. Many 'sockets' can use the same IP and a port number.

Giving a bigger dripper can extract more amounts at once. Basically more water and more bean powders are collected. This means TCP_CORK.

Using TCP_CORK to a broad range without SO_REUSEPORT can worsen average RTT. Also, using too many sockets after applying SO_REUSEPORT can drain Unix sockets. Simply enabling both increases two metrics at once: RPS and network transfer rate. When each barista is using a big dripper, if somebody's extraction ends up early, eight customers can get their cups. Naturally, that barista can prepare for other orders right after those cups.

How can we experiment with this with a real code? Let's dive into this.

Ran wrk -t10 -c100 -d30s http://localhost:8080/.

Source

#define _GNU_SOURCE 1 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/tcp.h>

#define WORKER_POOL 4
#define TCP_PORT 8080

/// set 1 to turn on each options. 
/// set 0 to turn off.
typedef struct Options {
    int so_reuseport;
    int tcp_cork;
} Opt_t;

/**
 * @brief check errno before writing to a client socket.
 */
static inline bool writeCheck(int c_fd, int n) {
    switch((int)(n < 0)) {
        case 0:
            return true;
            break;
        case 1:
            switch(errno) {
                case EPIPE:
                case ECONNRESET:
                    fprintf(stderr, "client disconnected a connection: errno(%d)\r\n", errno);
                    return false;
                case EINTR:
                    return true;
                default:
                    fprintf(stderr, "crash during write call: errno(%d)", errno);
                    return false;
            }
    }
}

/**
 * @brief implement a client loop.
 * @param s_fd server-side file descriptor
 * @param opt options including TCP_CORK and SO_REUSEPORT
 */
void clientLoop(int s_fd, Opt_t opt) {
    while(true) {
        int c_fd = accept(s_fd, NULL, NULL);
        if(c_fd < 0) continue;

        // serve requests on this connection until the client hangs up.
        while(true) {
            char req[4096];
            ssize_t r = read(c_fd, req, sizeof(req));
            if(r <= 0) break;

            switch((int)(opt.tcp_cork)) {
                case 0:
                    // writes small chunks
                    int n = write(c_fd, "HTTP/1.1 200 OK\r\n", 17);
                    if(!writeCheck(c_fd, n)) goto done;
                    n = write(c_fd, "Content-Type: text/plain\r\nContent-Length: 42\r\n\r\n", 49);
                    if(!writeCheck(c_fd, n)) goto done;
                    n = write(c_fd, "Data payload chunk 1\n", 21);
                    if(!writeCheck(c_fd, n)) goto done;
                    n = write(c_fd, "Data payload chunk 2\n", 21);
                    if(!writeCheck(c_fd, n)) goto done;
                    break;
                case 1:
                    int cork = 1;
                    // wait until cork == 0
                    setsockopt(c_fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
                    write(c_fd, "HTTP/1.1 200 OK\r\n", 17);
                    write(c_fd, "Content-Type: text/plain\r\nContent-Length: 42\r\n\r\n", 49);
                    write(c_fd, "Data payload chunk 1\n", 21);
                    write(c_fd, "Data payload chunk 2\n", 21);
                    cork = !cork;
                    setsockopt(c_fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
                    break;
                default:
                    break;
            }
        }
done:
        // close client fd
        close(c_fd);
    }
}

/**
 * @brief run worker loop.
 * @param w_id worker-side index 
 * @param opt options including TCP_CORK and SO_REUSEPORT
 */
void runWorker(int w_id, Opt_t opt) {
    // A TCP socket to bind
    printf("worker %d has started\r\n", w_id);
    int s_fd = socket(AF_INET, SOCK_STREAM, 0);
    int one = 1;
    setsockopt(s_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    switch(opt.so_reuseport) {
        case 1:
            setsockopt(s_fd, SOL_SOCKET, SO_REUSEPORT, &opt.so_reuseport, sizeof(int));
            break;
        default:
            break;
    }
    struct sockaddr_in addr;
    // This experiment uses IPv4 Address. AF_INET6 is disabled in here.
    addr.sin_family = AF_INET;
    // Allows all NIC address.
    // Selects any NIC device available on a machine.
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(TCP_PORT);
    bind(s_fd, (struct sockaddr *)&addr, sizeof(addr));
    signal(SIGPIPE, SIG_IGN);
    // queue len == 128
    listen(s_fd, 128);
    char ip_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &addr.sin_addr, (char *)ip_str, sizeof(ip_str));
    printf("listen %s:%d\r\n", ip_str, TCP_PORT);
    clientLoop(s_fd, opt);
}

/// main function starts.
int main(int argc, char **argv) {
    Opt_t opt;
    memset(&opt, 0, sizeof(opt));
    /* these two options are adjusted for each benchmark. */
    opt.so_reuseport = argc > 1 ? atoi(argv[1]) : 0;
    // please look at the switch in clientLoop
    opt.tcp_cork = argc > 2 ? atoi(argv[2]) : 0;
    // when SO_REUSEPORT is enabled multi-workers can share the same port.
    // in this source, http://localhost:8080 address is shared by four workers.
    if(opt.so_reuseport) {
        // Worker i: w_id i
        for(int i = 0; i < WORKER_POOL; ++i) {
            if(!fork()) {
                runWorker(i, opt);
                exit(0);
            }
        }
    }
    else
        runWorker(0, opt);

    while(1) pause();
    return 0;
}

benchmark_results (1).png

Vanilla

wrk -t10 -c100 -d30s http://localhost:8080/
Running 30s test @ http://localhost:8080/
  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    44.49ms     2.20ms  48.07ms   85.80%
    Req/Sec    22.46      4.31    30.00     75.42%
  676 requests in 30.10s, 71.31KB read
Requests/sec:     22.46
Transfer/sec:      2.37KB

SO_REUSEPORT only (4 workers)

Running 30s test @ http://localhost:8080/
  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    45.01ms     2.49ms  48.11ms   72.46%
    Req/Sec    88.85     12.51   121.00     82.06%
  2672 requests in 30.10s, 281.88KB read
Requests/sec:     88.77
Transfer/sec:      9.37KB

TCP_CORK only

Running 30s test @ http://localhost:8080/
  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    21.38us    16.81us   4.03ms   99.48%
    Req/Sec    44.88k     1.43k   47.22k    83.06%
  1343783 requests in 30.10s, 138.41MB read
Requests/sec:  44645.87
Transfer/sec:      4.60MB

TCP_CORK+SO_REUSEPORT

Running 30s test @ http://localhost:8080/
  10 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    26.31us    22.58us   4.23ms   99.52%
    Req/Sec    72.22k    33.77k  115.72k    56.15%
  4323980 requests in 30.10s, 445.36MB read
Requests/sec: 143653.90
Transfer/sec:     14.80MB

benchmark results

Analysis

As you can see, a benchmark with TCP_CORK transfers much more data per second.

Vanilla TCP_CORK
2.37 KB/s 4.60 MB/s

Also, enabling TCP_CORK increased RPS a lot, from 22.5 to 44645.9. The latency is 44.49ms without it and 21.38us with it.

It's exactly the same as this sentence at a coffee shop:

When you use a small dripper eight times, you need more time. But when you use a big dripper, huge amounts are extracted for just one time.

Vanilla SO_REUSEPORT
22.5/s 88.8/s

When we enable SO_REUSEPORT, RPS increases: 22.5 -> 88.8.

It's exactly the same as this enhancement at a coffee shop:

When you try single serving V60 drippers with eight baristas, it takes less than giving a dripper to a senior barista.


Now enable both of them. Let's look at whether these sentences are valid:

But each barista should prepare for each extractions when many orders are waiting per person since a dripper is small. Using TCP_CORK to a broad range without SO_REUSEPORT can worsen Average RTT. Also, using too many sockets after applying SO_REUSEPORT can drain Unix sockets. Simply enabling both increases two metrics at once: RPS and network transfer rate. When each barista is using a big dripper, if somebody's extraction ends up early, eight customers can get their cups. Naturally, that barista can prepare for other orders right after those cups.

Let's see the RPS metric.

SO_REUSEPORT+TCP_CORK SO_REUSEPORT
143653.9/s 88.8/s

Let's see the network transfer rate too.

SO_REUSEPORT+TCP_CORK SO_REUSEPORT
14.80 MB/s 9.37 KB/s

Woah. Both are good.

Enabling both accomplished the best effort.

Those two analogies are nice enough to describe how it works.

Knowing what's happening beyond exaggerated 'Blazingly Fast' code

When we use LLMs to refactor a codebase, we sometimes cannot understand why it's faster. Some people just merge it, and some reject it because 'they could not understand.' But the meaning of that source code is often neglected.

When we understand what they are doing, we can imagine what it affects when some accidents happen. When we don't try to understand, we accept some sloppy patches. And sometimes, we reject some requests that are actually better.

Applying TCP_CORK and SO_REUSEPORT is cheap. But understanding their behavior with experiments can easily lead us to be creative on a bottleneck.

Some say coding with our ten fingers is the only way to understand 'the truth of engineering.' But we can't learn anything when we just code without thinking on our own. Reading books like TAOCP may give us inspirations. But knowing what we've done is quite separated from those 'AI ethics and digital monads.'

Back

Comments

No comments yet.