Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Practical Use of SO_REUSEADDR and SO_REUSEPORT in TCP Server Development

Tech 1

This guide clarifies what SO_REUSEADDR and SO_REUSEPORT actually do on TCP sockets and demonstrates how to use them for common tasks such as graceful restarts and running multiple listeners on the same port.

What SO_REUSEADDR enables

On most Unix-like systems, SO_REUSEADDR affects TCP sockets in several ways:

  • Lets a server bind a well-known port even if sockets in TIME_WAIT or established connections referencing that local port still exist (typical for graceful restarts where workers continue serving existing clients).
  • Permits multiple server instances to bind the same port if they bind distinct local IP addresses (e.g., one instance on 127.0.0.1:PORT and another on 0.0.0.0:PORT is not allowed; one on 192.0.2.10:PORT and another on 192.0.2.11:PORT is allowed).
  • Allows a single process to create multiple sockets bound to the same port when each socket is bound to a different local IP.
  • For UDP (not TCP), some systems allow a complete duplicate bind (same IP and same port on different sockets) with SO_REUSEADDR alone.

The first item is what most TCP servers rely on to restart without dropping existing connections.

What SO_REUSEPORT enables

  • Enables multiple independent sockets to bind the exact same IP:port simultaneously—but only if every socket sets SO_REUSEPORT prior to bind.
  • For multicast address binds, SO_REUSEADDR and SO_REUSEPORT are treated equivalently by many kernels.

On Linux, SO_REUSEPORT (kernel 3.9+) also provides better accept-load distribution among listeners and avoids the classic thundering herd problem by distributing incoming connections across sockets.

Focus in this article

  • Using SO_REUSEADDR to restart a listener while existing connections remain served by old workers
  • Using SO_REUSEPORT to run multiple listener processes on the same IP:port concurrently

These techniques are commonly used by servers that perform smooth reloads or upgrades (e.g., nginx-style graceful restarts).


Experiment 1: Graceful restart with SO_REUSEADDR

Behavior to observe:

  • Without any reuse option, a second process binding the same address fails with EADDRINUSE.
  • If only SO_REUSEADDR is set but the original listening socket stays open, a second process will still fail to bind the same IP:port.
  • If SO_REUSEADDR is set, and the first process accepts a client and then closes only its listening socket (keeping the accepted connection open), a second process can successfully bind the same IP:port and take over new connections while the first continues serving existing clients.

Example program (C)

Compile two binaries from the same source and run them in sequence to reproduce the behavior.

// reuseaddr_server.c
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

static void die(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

static int open_listener(uint16_t port, int qlen) {
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) die("socket");

    int one = 1;
    if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
        die("setsockopt(SO_REUSEADDR)");

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY); // kernel chooses the local address
    addr.sin_port = htons(port);

    if (bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
        die("bind");

    if (listen(lfd, qlen) < 0)
        die("listen");

    return lfd;
}

int main(void) {
    const uint16_t port = 9527;
    const int backlog = 128;

    int lfd = open_listener(port, backlog);
    printf("listening on 0.0.0.0:%u\n", port);

    // Wait for a single client, then close only the listening socket
    struct sockaddr_in peer;
    socklen_t plen = sizeof(peer);
    int cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
    if (cfd < 0) die("accept");

    char ip[INET_ADDRSTRLEN];
    if (!inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip))) strcpy(ip, "?");
    printf("accepted from %s:%u\n", ip, ntohs(peer.sin_port));

    // Simulate old worker keeping the connection alive while new process takes over
    close(lfd); // critical: allow another process to bind now

    // Keep the accepted connection open for a while to observe coexistence
    sleep(1200);

    close(cfd);
    return 0;
}

Build two copies and run them in separate terminals:

gcc -O2 -Wall -Wextra reuseaddr_server.c -o s1
gcc -O2 -Wall -Wextra reuseaddr_server.c -o s2
  • Start ./s1. Connect once using a client (e.g., nc 127.0.0.1 9527).
  • After the first accept succeeds, s1 closes its listening socket but leaves the client connection open.
  • Now start ./s2; it should bind successfully and accept new connections on the same port while s1 continues serving the original client.

Experiment 2: Parallel listeners with SO_REUSEPORT

SO_REUSEPORT lets multiple unrelated processes listen on the same IP:port at the same time. On Linux (>= 3.9), the kernel load-balances connection attempts across these sockets, minimizing wakeups and reducing the thundering herd.

Example program (C)

The following server prints its PID when a connection is accepted so you can see load distribution when two instances run concurrently.

// reuseport_server.c
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

static void die(const char *msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

int main(void) {
    const uint16_t port = 9527;
    const int backlog = 256;

    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) die("socket");

    int one = 1;
    if (setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) < 0)
        die("setsockopt(SO_REUSEPORT)");

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);

    if (bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
        die("bind");

    if (listen(lfd, backlog) < 0)
        die("listen");

    printf("pid %ld listening on 0.0.0.0:%u\n", (long)getpid(), port);

    for (;;) {
        struct sockaddr_in peer;
        socklen_t plen = sizeof(peer);
        int cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
        if (cfd < 0) die("accept");

        char ip[INET_ADDRSTRLEN];
        if (!inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip))) strcpy(ip, "?");
        printf("pid %ld accepted %s:%u\n", (long)getpid(), ip, ntohs(peer.sin_port));
        close(cfd); // close immediately; adjust as needed
    }
}

Build two copies and start both:

gcc -O2 -Wall -Wextra reuseport_server.c -o s1
gcc -O2 -Wall -Wextra reuseport_server.c -o s2
./s1 &
./s2 &

Initiate multiple client connections (e.g., for i in $(seq 1 10); do nc -z 127.0.0.1 9527 & done). You should see accept logs from both processes, showing that connections are distributed across listeners.


Notes and pitfalls

  • Kernel support: Linux requires 3.9+ for SO_REUSEPORT; older kernels may not implement it or may implement earlier semantics. macOS and BSDs support SO_REUSEPORT but with historical differences; consult your OS documnetation.
  • All listeners must set SO_REUSEPORT before bind, otherwise the kernel rejects duplicate binds.
  • SO_REUSEADDR alone does not allow two TCP sockets to bind the exact same IP:port at the same time; it mainly relaxes rules around reusing a port where connections exist or are in TIME_WAIT, and around binding on different local IPs.
  • For multicast UDP, SO_REUSEADDR and SO_REUSEPORT are often treated similarly so multiple receivers can bind the same group:port.
  • When building production systems, combine these options with proper signal handling and orderly listener handoff to implement graceful upgrades without connection drops.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.