How Redis Achieves High‑Performance Networking with a Single‑Threaded Event Loop

This article explains how Redis uses Linux epoll and a single‑threaded event loop to handle millions of connections efficiently, covering the creation of the epoll object, server initialization, event registration, the main processing loop, and the mechanisms for reading, writing, and managing pending tasks.

dbaplus Community
dbaplus Community
dbaplus Community
How Redis Achieves High‑Performance Networking with a Single‑Threaded Event Loop

Understanding I/O Multiplexing

Before diving into Redis, the article introduces epoll , the Linux kernel mechanism that allows a single thread to monitor many sockets simultaneously, eliminating the need for one thread per connection.

Redis Server Startup Initialization

Redis’s entry point is int main(int argc, char **argv) in src/server.c. The main function calls initServer() to set up the event loop and then aeMain(server.el) to start the endless event‑processing loop.

# git clone https://github.com/redis/redis
# cd redis
# git checkout -b 5.0.0 5.0.0

The core of initServer() performs three essential tasks:

Create an epoll object.

Bind the configured listening port.

Register the accept handler for the listening socket.

1. Creating the epoll Object

The function aeCreateEventLoop(int setsize) allocates an aeEventLoop structure and calls aeApiCreate(eventLoop), which in turn invokes epoll_create(1024) to obtain the kernel epoll file descriptor.

//file: src/ae.c
aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop;
    eventLoop = zmalloc(sizeof(*eventLoop));
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    aeApiCreate(eventLoop);
    return eventLoop;
}

//file: src/ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));
    state->epfd = epoll_create(1024);
    eventLoop->apidata = state;
    return 0;
}

2. Binding the Listening Port

Redis calls

listenToPort(server.port, server.ipfd, &server.ipfd_count)

, which loops over the configured bind addresses and invokes anetTcpServer (a thin wrapper around socket, bind, and listen) to create the listening socket.

//file: src/redis.c
int listenToPort(int port, int *fds, int *count) {
    for (j = 0; j < server.bindaddr_count || j == 0; j++) {
        fds[*count] = anetTcpServer(server.neterr, port, NULL, server.tcp_backlog);
    }
}

//file: src/anet.c
int anetTcpServer(char *err, int port, char *bindaddr, int backlog) {
    return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}

static int _anetTcpServer(...) {
    // set SO_REUSEADDR, bind, listen ...
    anetSetReuseAddr(err, s);
    bind(s, sa, len);
    listen(s, backlog);
}

3. Registering the Accept Handler

For each listening socket, aeCreateFileEvent registers acceptTcpHandler as the readable callback.

//file: src/server.c
void initServer() {
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    listenToPort(server.port, server.ipfd, &server.ipfd_count);
    for (j = 0; j < server.ipfd_count; j++) {
        aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler, NULL);
    }
}

Redis Event‑Processing Loop

The infinite loop in aeMain repeatedly:

Calls beforesleep to flush pending write buffers.

Invokes aeProcessEvents, which internally calls epoll_wait to discover readable or writable events.

//file: src/ae.c
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

1. epoll_wait discovers events

When epoll_wait reports a ready socket, the loop retrieves the corresponding aeFileEvent from eventLoop->events and invokes the stored read or write callbacks.

2. Handling New Connections

The readable callback for the listening socket is acceptTcpHandler. It performs:

Accept the new TCP connection.

Create a redisClient object.

Register a readable event for the new client socket.

//file: src/networking.c
void acceptTcpHandler(aeEventLoop *el, int fd, ...) {
    int cfd = anetTcpAccept(server.neterr, fd, ...);
    acceptCommonHandler(cfd, 0);
}

static void acceptCommonHandler(int fd, int flags) {
    redisClient *c = createClient(fd);
    // further initialization ...
}

3. Processing Client Commands

When a client socket becomes readable, readQueryFromClient is invoked. It reads the request, parses the command, executes it via call, and queues the reply.

//file: src/networking.c
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, ...) {
    redisClient *c = (redisClient*)privdata;
    processInputBufferAndReplicate(c);
}

void processInputBufferAndReplicate(client *c) {
    processInputBuffer(c);
}

void processInputBuffer(redisClient *c) {
    processCommand(c);
}

The processCommand function looks up the command in redisCommandTable and either queues it (for MULTI) or calls the command implementation directly.

//file: src/server.c
int processCommand(redisClient *c) {
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    if (c->flags & CLIENT_MULTI) {
        queueMultiCommand(c);
    } else {
        call(c, CMD_CALL_FULL);
    }
    return C_OK;
}

For a GET request, getCommand eventually calls addReplyBulk to place the value into the client’s output buffer.

//file: src/t_string.c
void getCommand(client *c) { getGenericCommand(c); }

int getGenericCommand(client *c) {
    robj *o = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp]);
    if (o == NULL) return C_OK;
    addReplyBulk(c, o);
    return C_OK;
}

4. Writing Replies

Before each epoll_wait , beforesleep runs handleClientsWithPendingWrites , which iterates over server.clients_pending_write and attempts to flush each client’s buffer via writeToClient . If the socket’s send buffer cannot accept all data, the client remains in the pending list and a writable event is re‑registered.

//file: src/server.c
void beforeSleep(struct aeEventLoop *eventLoop) {
    handleClientsWithPendingWrites();
}

int handleClientsWithPendingWrites(void) {
    listIter li; listNode *ln;
    listRewind(server.clients_pending_write, &li);
    while ((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags &= ~CLIENT_PENDING_WRITE;
        listDelNode(server.clients_pending_write, ln);
        writeToClient(c->fd, c, 0);
        if (clientHasPendingReplies(c)) {
            aeCreateFileEvent(server.el, c->fd, ae_flags, sendReplyToClient, c);
        }
    }
}

High‑Performance Summary

Redis achieves tens of thousands of QPS with a single thread by:

Using epoll to multiplex thousands of connections onto one event loop.

Separating connection acceptance, command parsing, execution, and reply buffering into distinct callbacks.

Deferring writes to a pending‑write queue and only registering writable events when the kernel cannot send all data at once.

Understanding initServer and aeMain provides a solid foundation for grasping Redis’s networking architecture.

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.

Backend Developmenthigh performanceNetwork programmingepollevent loopSingle Thread
dbaplus Community
Written by

dbaplus Community

Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.

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.