Understanding and Solving the Linux Thundering Herd Problem

This article explains the Linux thundering herd phenomenon, its impact on processes, threads, and epoll, demonstrates it with C code examples, analyzes system overhead, and presents mitigation techniques such as accept mutex, SO_REUSEPORT, and proper locking to improve server performance.

Open Source Linux
Open Source Linux
Open Source Linux
Understanding and Solving the Linux Thundering Herd Problem

What is the thundering herd effect?

The thundering herd (also called the thunderstorm effect) occurs when multiple processes or threads are blocked waiting for the same event; when the event occurs, all of them are awakened, but only one can acquire the event and handle it, while the others go back to sleep, causing wasted CPU cycles and context switches.

An analogy is a group of pigeons startled by a single grain of seed: only one pigeon gets the seed while the rest return to sleep.

What does the thundering herd consume?

1) Excessive, useless scheduling and context‑switch overhead for the awakened processes/threads, degrading overall system performance.

2) Additional locking required to ensure only one entity handles the event, further increasing system load.

The true nature of the thundering herd

From both process and thread perspectives, the effect can be observed in three common scenarios: accept() calls, epoll_wait() , and thread condition broadcasts.

1) accept() thundering herd

Scenario: a parent creates a listening socket, forks several child processes, and each child blocks on accept(). When a new connection arrives, all children are awakened, but only one succeeds in accepting the connection.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <string.h>
#include <netinet/in.h>
#include <unistd.h>

#define PROCESS_NUM 10
int main() {
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    int connfd;
    int pid;
    char sendbuff[1024];
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(1234);
    bind(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(fd, 1024);
    for (int i = 0; i < PROCESS_NUM; ++i) {
        pid = fork();
        if (pid == 0) {
            while (1) {
                connfd = accept(fd, NULL, NULL);
                snprintf(sendbuff, sizeof(sendbuff), "process %d accepted
", getpid());
                send(connfd, sendbuff, strlen(sendbuff)+1, 0);
                printf("process %d accept success
", getpid());
                close(connfd);
            }
        }
    }
    wait(0);
    return 0;
}

Running this program and connecting with telnet shows that only one process prints a success message, confirming that the kernel wakes only one process for accept() in modern Linux (2.6+).

2) epoll_wait() thundering herd

When multiple processes block on epoll_wait() for the same listening socket, a new connection can wake all waiting processes. Only one will successfully accept(), the others receive EAGAIN.

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <errno.h>

#define PROCESS_NUM 10
#define MAXEVENTS 64

int sock_create_bind(char *port) {
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(port));
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(sock_fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    return sock_fd;
}

int make_nonblocking(int fd) {
    int val = fcntl(fd, F_GETFL);
    val |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, val) < 0) {
        perror("fcntl set");
        return -1;
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("usage: %s [port]
", argv[1]); exit(1); }
    int sock_fd = sock_create_bind(argv[1]);
    make_nonblocking(sock_fd);
    listen(sock_fd, SOMAXCONN);
    int epoll_fd = epoll_create(MAXEVENTS);
    struct epoll_event event, *events = calloc(MAXEVENTS, sizeof(event));
    event.data.fd = sock_fd; event.events = EPOLLIN;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event);
    for (int i = 0; i < PROCESS_NUM; ++i) {
        int pid = fork();
        if (pid == 0) {
            while (1) {
                int num = epoll_wait(epoll_fd, events, MAXEVENTS, -1);
                printf("process %d returned from epoll_wait
", getpid());
                sleep(2);
                for (int j = 0; j < num; ++j) {
                    if (events[j].data.fd == sock_fd) {
                        if (accept(sock_fd, NULL, NULL) < 0)
                            printf("process %d accept failed! %s
", getpid(), strerror(errno));
                        else
                            printf("process %d accept successful!
", getpid());
                    }
                }
            }
        }
    }
    wait(0);
    free(events);
    close(sock_fd);
    return 0;
}

Strace shows that only one process gets the connection (the others receive EAGAIN), confirming a partial thundering herd. Adding a sleep(2) after epoll_wait() forces all processes to be awakened, demonstrating the effect.

3) Thread thundering herd

When a condition variable is broadcast (e.g., pthread_cond_broadcast()), all waiting threads are awakened, but only one obtains the resource. The usual remedy is to protect the critical section with a mutex.

printf("Initial red‑packet: <count:%d amount:%d.%02d>
", item.number, item.total/100, item.total%100);
pthread_cond_broadcast(&temp.cond); // wake all threads
pthread_mutex_unlock(&temp.mutex);
sleep(1);

How to solve the thundering herd?

Common approaches include:

1) Nginx accept mutex

Nginx workers use an accept mutex (configurable via accept_mutex) so that only one worker calls accept() at a time. The mutex is non‑blocking; if a worker cannot obtain it, it backs off, reducing wake‑ups.

void ngx_process_events_and_timers(ngx_cycle_t *cycle) {
    if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;
        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) return;
            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;
            } else {
                if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }
    ngx_process_events(cycle, timer, flags);
    if (ngx_posted_accept_events) {
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    }
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }
    if (delta) ngx_event_expire_timers();
}

2) SO_REUSEPORT

Since Linux 3.9, the SO_REUSEPORT socket option allows multiple processes or threads to bind to the same IP:port. The kernel then load‑balances incoming connections among the sockets, eliminating lock contention on accept().

With SO_REUSEPORT, each worker has its own listening socket, no mutex is needed, and the kernel performs per‑connection hashing to select the target worker.

3) Comparison of traditional multi‑process/thread servers vs. SO_REUSEPORT

Traditional models suffer from a single listener bottleneck, lock contention on the listening socket, cache‑line bouncing, and uneven load distribution. SO_REUSEPORT removes these issues by providing lock‑free parallel accept and kernel‑level load balancing.

Conclusion

The thundering herd effect can degrade server performance in accept, epoll, and thread‑based designs. Modern Linux kernels mitigate the accept case, but epoll and thread wake‑ups may still exhibit the problem. Using accept mutexes, SO_REUSEPORT, and careful locking are effective ways to eliminate unnecessary wake‑ups and improve scalability.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Linuxepollthundering herdserver performanceacceptSO_REUSEPORT
Open Source Linux
Written by

Open Source Linux

Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.