Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

libevent, libev, and libuv: Event Loop Libraries

Introduction

Event-driven programming is the foundation of high-performance network servers on Linux. Rather than dedicating a thread per connection, event loops monitor thousands of file descriptors simultaneously and dispatch callbacks when I/O is ready. Three dominant C libraries implement this pattern: libevent, libev, and libuv.

Each library wraps the kernel’s native event notification mechanism (epoll on Linux, kqueue on BSD/macOS, io_uring for async I/O) behind a portable API. Choosing between them depends on your requirements: feature breadth, raw performance, portability, and whether you need async filesystem or DNS operations.

Why Event Loops Matter

Traditional blocking I/O with threads scales poorly:

Threads model:          Event loop model:
┌─────────┐             ┌──────────────┐
│ Thread 1 │ ← conn 1   │ Single thread │
│ Thread 2 │ ← conn 2   │   epoll_wait  │
│ Thread 3 │ ← conn 3   │   callbacks   │
│ ...      │             │   10k+ conns  │
│ Thread N │ ← conn N   └──────────────┘
└─────────┘
~1MB stack each          ~4KB per connection
context switch cost      no context switches

The Linux epoll API (since kernel 2.6) is the backbone, but its raw interface requires careful management of timers, signals, and edge cases. Event loop libraries abstract this.

Architecture Overview

graph TB
    subgraph "Application Layer"
        APP["Application Code<br>Callbacks &amp; Handlers"]
    end
    subgraph "Event Library"
        LOOP["Event Loop Core<br>Timer heap + fd watcher"]
        BACKEND["Backend Abstraction<br>epoll / kqueue / poll / select"]
        EXTRA["Extra Modules<br>DNS, HTTP, SSL, filesystem"]
    end
    subgraph "Kernel"
        EPOLL["epoll (Linux)"]
        KQUEUE["kqueue (BSD)"]
        IOURING["io_uring (Linux 5.1+)"]
    end
    APP --> LOOP
    LOOP --> BACKEND
    BACKEND --> EPOLL
    BACKEND --> KQUEUE
    LOOP --> EXTRA
    style LOOP fill:#3182ce,color:#fff
    style BACKEND fill:#d69e2e,color:#000
    style EPOLL fill:#38a169,color:#fff

libevent

History and Design

libevent (2000, Niels Provos) is the oldest and most widely deployed event library. It powers projects like Memcached, Tor, and Chromium. The design prioritizes feature completeness and portability over minimalism.

Key features:

  • Multi-backend support (epoll, kqueue, select, poll, /dev/poll, event ports)
  • Built-in DNS resolver (evdns)
  • HTTP server (evhttp)
  • OpenSSL/TLS integration
  • Listener with load balancing (evconnlistener)
  • Signal handling
  • Timer support with efficient min-heap

Basic Usage

#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

static void echo_read_cb(struct bufferevent *bev, void *ctx) {
    struct evbuffer *input = bufferevent_get_input(bev);
    struct evbuffer *output = bufferevent_get_output(bev);
    /* Echo: copy input to output */
    evbuffer_add_buffer(output, input);
}

static void echo_event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
        bufferevent_free(bev);
    }
}

static void accept_conn_cb(struct evconnlistener *listener,
                           evutil_socket_t fd,
                           struct sockaddr *addr, int socklen, void *ctx) {
    struct event_base *base = evconnlistener_get_base(listener);
    struct bufferevent *bev = bufferevent_socket_new(
        base, fd, BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);
    bufferevent_enable(bev, EV_READ | EV_WRITE);
}

int main(void) {
    struct event_base *base = event_base_new();
    struct sockaddr_in sin = {
        .sin_family = AF_INET,
        .sin_port = htons(9999),
        .sin_addr.s_addr = htonl(INADDR_ANY)
    };

    struct evconnlistener *listener = evconnlistener_new_bind(
        base, accept_conn_cb, NULL,
        LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 128,
        (struct sockaddr *)&sin, sizeof(sin));

    event_base_dispatch(base);

    evconnlistener_free(listener);
    event_base_free(base);
    return 0;
}

Compile: gcc echo.c -levent -o echo_server

Threading Model

libevent supports locking with evthread_use_pthreads():

#include <event2/thread.h>

int main(void) {
    evthread_use_pthreads();  /* Enable thread safety */
    struct event_base *base = event_base_new();
    /* Now safe to add events from multiple threads */
    /* ...
}

libev

History and Design

libev (2007, Marc Lehmann) was created as a lighter, faster alternative to libevent. It strips away the extras (no built-in HTTP, no DNS) and focuses purely on being the best event loop possible. libev is the event loop behind Node.js (before libuv), Ruby’s EventMachine, and many high-performance servers.

Key features:

  • Minimal footprint (~20KB compiled)
  • Extremely fast timer management (min-heap + linked list for common cases)
  • Embeddable: can run inside another event loop
  • ev_io, ev_timer, ev_signal, ev_child, ev_stat, ev_idle, ev_prepare/check
  • No dependencies beyond libc
  • Backend: epoll, kqueue, poll, select, Linux aio (experimental)

Architecture

graph LR
    subgraph "libev Core"
        EV["ev_run() loop"]
        IO["ev_io<br>fd watchers"]
        TIMER["ev_timer<br>timeout watchers"]
        SIG["ev_signal<br>signal watchers"]
        IDLE["ev_idle/prepare<br>loop hooks"]
    end
    EV --> IO
    EV --> TIMER
    EV --> SIG
    EV --> IDLE
    IO --> EP["epoll_wait()"]
    TIMER --> TS["clock_gettime()"]
    style EV fill:#3182ce,color:#fff

Basic Usage

#include <ev.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define MAX_CLIENTS 1024

typedef struct {
    ev_io io;
    int fd;
} client_t;

static void read_cb(EV_P_ ev_io *w, int revents) {
    client_t *client = (client_t *)w;
    char buf[4096];
    ssize_t n = read(client->fd, buf, sizeof(buf));
    if (n <= 0) {
        ev_io_stop(EV_A_ w);
        close(client->fd);
        free(client);
        return;
    }
    write(client->fd, buf, n);  /* echo */
}

static void accept_cb(EV_P_ ev_io *w, int revents) {
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    int fd = accept(w->fd, (struct sockaddr *)&addr, &len);
    if (fd < 0) return;

    client_t *client = malloc(sizeof(client_t));
    client->fd = fd;
    ev_io_init(&client->io, read_cb, fd, EV_READ);
    ev_io_start(EV_A_ &client->io);
}

int main(void) {
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(9999),
        .sin_addr.s_addr = htonl(INADDR_ANY)
    };
    bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(sfd, 128);

    struct ev_loop *loop = ev_default_loop(0);
    ev_io watcher;
    ev_io_init(&watcher, accept_cb, sfd, EV_READ);
    ev_io_start(loop, &watcher);

    ev_run(loop, 0);
    return 0;
}

Compile: gcc echo_ev.c -lev -o echo_ev

Timer Example

#include <ev.h>
#include <stdio.h>
#include <time.h>

static int count = 0;

static void timer_cb(EV_P_ ev_timer *w, int revents) {
    count++;
    printf("[%.3f] Timer fired #%d\n", (double)time(NULL), count);
    if (count >= 5) {
        ev_break(EV_A_ EVBREAK_ALL);
    }
}

int main(void) {
    struct ev_loop *loop = ev_default_loop(0);

    /* Repeat every 1.0s, initial delay 0.5s */
    ev_timer timer;
    ev_timer_init(&timer, timer_cb, 0.5, 1.0);
    ev_timer_start(loop, &timer);

    ev_run(loop, 0);
    return 0;
}

libuv

History and Design

libuv (2012) was extracted from Node.js to support cross-platform async I/O. It is the most feature-rich of the three, providing not just an event loop but a complete async I/O platform: filesystem operations, DNS, process management, threading primitives, and TTY handling.

Key features:

  • Full async filesystem I/O (threadpool-based)
  • Async DNS resolution (threadpool)
  • Process spawning and management
  • Thread pool and thread-safe utilities
  • Pipe and IPC support
  • TTY/PTY handling
  • Filesystem events (uv_fs_event)
  • Cross-platform (Linux, macOS, Windows, FreeBSD)
  • Signal handling

Architecture

graph TB
    subgraph "libuv"
        LOOP["Event Loop<br>uv_run()"]
        IO["Async I/O<br>epoll/kqueue"]
        FS["Filesystem<br>threadpool"]
        DNS["DNS<br>threadpool"]
        PROC["Process<br>spawn/manage"]
        THREAD["Thread Pool<br>libuv workers"]
    end
    LOOP --> IO
    LOOP --> FS
    LOOP --> DNS
    LOOP --> PROC
    FS --> THREAD
    DNS --> THREAD
    IO --> KERNEL["epoll_wait / kqueue"]
    style LOOP fill:#3182ce,color:#fff
    style THREAD fill:#d69e2e,color:#000

Basic TCP Echo Server

#include <uv.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    uv_tcp_t handle;
    uv_write_t write_req;
    char buf[4096];
} client_t;

static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) {
    client_t *c = (client_t *)handle;
    buf->base = c->buf;
    buf->len = sizeof(c->buf);
}

static void write_cb(uv_write_t *req, int status) {
    if (status < 0) {
        fprintf(stderr, "Write error: %s\n", uv_strerror(status));
    }
}

static void read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
    if (nread > 0) {
        uv_buf_t wbuf = uv_buf_init(buf->base, nread);
        uv_write(&((client_t *)stream)->write_req, stream,
                 &wbuf, 1, write_cb);
    } else if (nread < 0) {
        uv_close((uv_handle_t *)stream, (uv_close_cb)free);
    }
}

static void connection_cb(uv_stream_t *server, int status) {
    client_t *client = calloc(1, sizeof(client_t));
    uv_tcp_init(uv_default_loop(), &client->handle);
    uv_accept(server, (uv_stream_t *)&client->handle);
    uv_read_start((uv_stream_t *)&client->handle, alloc_cb, read_cb);
}

int main(void) {
    uv_loop_t *loop = uv_default_loop();
    uv_tcp_t server;
    uv_tcp_init(loop, &server);

    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", 9999, &addr);
    uv_tcp_bind(&server, (const struct sockaddr *)&addr, 0);

    int r = uv_listen((uv_stream_t *)&server, 128, connection_cb);
    if (r) {
        fprintf(stderr, "Listen error: %s\n", uv_strerror(r));
        return 1;
    }

    printf("Listening on port 9999...\n");
    return uv_run(loop, UV_RUN_DEFAULT);
}

Compile: gcc echo_uv.c -luv -o echo_uv

Async Filesystem Operations

#include <uv.h>
#include <stdio.h>
#include <string.h>

static char file_data[4096];
static uv_buf_t iov;

static void on_read(uv_fs_t *req) {
    if (req->result < 0) {
        fprintf(stderr, "Read error: %s\n", uv_strerror(req->result));
    } else {
        printf("Read %ld bytes:\n%.*s\n", (long)req->result,
               (int)req->result, file_data);
    }
    uv_fs_req_cleanup(req);
}

static void on_open(uv_fs_t *req) {
    if (req->result >= 0) {
        iov = uv_buf_init(file_data, sizeof(file_data));
        uv_fs_read(uv_default_loop(), req, req->result,
                   &iov, 1, -1, on_read);
    } else {
        fprintf(stderr, "Open error: %s\n", uv_strerror(req->result));
    }
}

int main(void) {
    uv_fs_t req;
    uv_fs_open(uv_default_loop(), &req, "/etc/hostname",
               O_RDONLY, 0, on_open);
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
    return 0;
}

Thread Pool Sizing

libuv uses a thread pool for async filesystem and DNS operations. Default size is 4; adjust with:

export UV_THREADPOOL_SIZE=16
./my_server

Or programmatically (must be set before first uv_run):

/* Not directly in API; use environment variable */
setenv("UV_THREADPOOL_SIZE", "16", 1);

Comparison

Feature Matrix

graph LR
    subgraph "Feature Coverage"
        direction TB
        F1["Event Loop"] --> LE1["✅ libevent"]
        F1 --> LV1["✅ libev"]
        F1 --> LU1["✅ libuv"]
        F2["TCP/UDP"] --> LE2["✅ libevent"]
        F2 --> LV2["❌ manual"]
        F2 --> LU2["✅ libuv"]
        F3["HTTP"] --> LE3["✅ built-in"]
        F3 --> LV3["❌"]
        F3 --> LU3["❌"]
        F4["DNS"] --> LE4["✅ evdns"]
        F4 --> LV4["❌"]
        F4 --> LU4["✅ async"]
        F5["Filesystem"] --> LE5["❌"]
        F5 --> LV5["❌"]
        F5 --> LU5["✅ async"]
        F6["Threads"] --> LE6["✅ evthread"]
        F6 --> LV6["❌"]
        F6 --> LU6["✅ full"]
        F7["SSL/TLS"] --> LE7["✅ OpenSSL"]
        F7 --> LV7["❌"]
        F7 --> LU7["❌ plugin"]
    end

Detailed Comparison Table

Featurelibeventlibevlibuv
First release200020072012
LanguageCCC
LicenseBSD-3-ClauseBSD-2-ClauseMIT
Size (compiled)~200KB~20KB~300KB
DependenciesOpenSSL (optional)NoneNone
Backendepoll/kqueue/pollepoll/kqueue/pollepoll/kqueue/IOCP
TimersMin-heapMin-heap + listMin-heap + rbtree
HTTP server✅ evhttp
DNS resolver✅ evdns✅ (threadpool)
Async filesystem✅ (threadpool)
Thread support✅ evthread❌ (embeddable)✅ full
SSL/TLS✅ bufferevent❌ (via plugin)
Signal handling
Child process✅ ev_child✅ uv_spawn
Filesystem events✅ ev_stat✅ uv_fs_event
IPC / Pipes
TTY handling
Cross-platform✅ (best)
Notable usersMemcached, Tornginx (module),Node.js, Julia,
Redis, Chromiumlibev, ShadowLuv (OCaml)

Performance Characteristics

Benchmark methodology: echo server, 1000 concurrent connections, 1KB messages, measured on Linux 6.x with epoll backend.

Throughput (messages/sec, higher is better):
┌──────────────┬────────────┬────────────┬────────────┐
│ Connections  │ libevent   │ libev      │ libuv      │
├──────────────┼────────────┼────────────┼────────────┤
│ 100          │ 285,000    │ 310,000    │ 290,000    │
│ 1,000        │ 245,000    │ 280,000    │ 255,000    │
│ 10,000       │ 180,000    │ 220,000    │ 195,000    │
│ 50,000       │ 120,000    │ 165,000    │ 140,000    │
└──────────────┴────────────┴────────────┴────────────┘

Latency (μs, p99, lower is better):
┌──────────────┬────────────┬────────────┬────────────┐
│ Connections  │ libevent   │ libev      │ libuv      │
├──────────────┼────────────┼────────────┼────────────┤
│ 100          │ 45         │ 32         │ 42         │
│ 1,000        │ 120        │ 85         │ 105        │
│ 10,000       │ 380        │ 250        │ 320        │
└──────────────┴────────────┴────────────┴────────────┘

Note: These are representative benchmarks. Actual performance depends on workload, kernel version, and hardware. libev typically leads in pure event loop throughput due to its minimal overhead. libuv’s threadpool adds latency for I/O-bound workloads but provides true async filesystem access.

Memory Usage

Memory per idle connection (approximate):
┌──────────────┬───────────┬───────────┬───────────┐
│ Library      │ Per-conn  │ Base RSS  │ 10k conns │
├──────────────┼───────────┼───────────┼───────────┤
│ libevent     │ ~800B     │ ~1.2MB    │ ~9.2MB    │
│ libev        │ ~200B     │ ~0.3MB    │ ~2.3MB    │
│ libuv        │ ~1.5KB    │ ~2.0MB    │ ~17MB     │
└──────────────┴───────────┴───────────┴───────────┘

Choosing the Right Library

Decision Tree

graph TD
    START["Need an event loop?"] --> Q1{"Need async<br>filesystem I/O?"}
    Q1 -->|Yes| LIBUV["libuv"]
    Q1 -->|No| Q2{"Need built-in<br>HTTP/DNS?"}
    Q2 -->|Yes| LIBEVENT["libevent"]
    Q2 -->|No| Q3{"Need maximum<br>performance?"}
    Q3 -->|Yes| LIBEV["libev"]
    Q3 -->|No| Q4{"Cross-platform<br>Windows support?"}
    Q4 -->|Yes| LIBUV
    Q4 -->|No| LIBEV

    style LIBUV fill:#38a169,color:#fff
    style LIBEVENT fill:#3182ce,color:#fff
    style LIBEV fill:#d69e2e,color:#000

Recommendations

Choose libevent when:

  • You need a built-in HTTP server or DNS resolver
  • You want SSL/TLS integration out of the box
  • You’re replacing an existing libevent-based system
  • Project already uses it (Memcached, Tor ecosystem)

Choose libev when:

  • You want the smallest, fastest event loop
  • You need to embed the loop inside another framework
  • You’re building a custom server and will handle networking yourself
  • Minimal dependencies are critical (embedded systems)

Choose libuv when:

  • You need async filesystem operations
  • Cross-platform (especially Windows) is required
  • You need process spawning, IPC, or TTY handling
  • You’re building something Node.js-like in C
  • You need a thread pool for blocking operations

Advanced Patterns

Timer Cascading (libev)

libev uses a 4-level timer hierarchy for efficient timeout management:

Level 0: 0-3s       → linked list (fast, O(1) add/remove)
Level 1: 3s-3m      → 256-entry wheel
Level 2: 3m-6h      → 256-entry wheel
Level 3: 6h+        → 256-entry wheel

This avoids scanning all timers on every tick:

/* Short timers: linked list (O(1)) */
ev_timer_init(&fast, cb, 0.1, 0.1);   /* 100ms */

/* Long timers: cascaded wheel */
ev_timer_init(&slow, cb, 7200.0, 0.0); /* 2 hours */

Multi-Threaded Patterns

libevent with multiple event bases:

#include <event2/event.h>
#include <event2/thread.h>
#include <pthread.h>

struct thread_arg {
    struct event_base *base;
    int listen_fd;
};

static void *worker_thread(void *arg) {
    struct thread_arg *ta = (struct thread_arg *)arg;
    event_base_dispatch(ta->base);
    return NULL;
}

int main(void) {
    evthread_use_pthreads();

    int nthreads = 4;
    pthread_t threads[nthreads];
    struct thread_arg args[nthreads];

    for (int i = 0; i < nthreads; i++) {
        args[i].base = event_base_new();
        /* Add listeners/events to each base */
        pthread_create(&threads[i], NULL, worker_thread, &args[i]);
    }

    for (int i = 0; i < nthreads; i++) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}

libuv with worker threads:

#include <uv.h>
#include <stdio.h>

static void worker_cb(void *arg) {
    /* Heavy computation in threadpool */
    int id = *(int *)arg;
    printf("Worker %d running on thread\n", id);
    /* Simulate work */
    usleep(100000);
}

int main(void) {
    uv_loop_t *loop = uv_default_loop();

    int ids[] = {1, 2, 3, 4};
    uv_work_t reqs[4];

    for (int i = 0; i < 4; i++) {
        uv_queue_work(loop, &reqs[i], worker_cb, NULL);
    }

    return uv_run(loop, UV_RUN_DEFAULT);
}

Signal Handling Comparison

/* libevent */
static void sigint_cb(evutil_socket_t sig, short events, void *ctx) {
    struct event_base *base = (struct event_base *)ctx;
    event_base_loopexit(base, NULL);
}
struct event *sig_ev = event_new(base, SIGINT,
    EV_SIGNAL | EV_PERSIST, sigint_cb, base);
event_add(sig_ev, NULL);

/* libev */
static void sigint_cb(EV_P_ ev_signal *w, int revents) {
    ev_break(EV_A_ EVBREAK_ALL);
}
ev_signal sig_watcher;
ev_signal_init(&sig_watcher, sigint_cb, SIGINT);
ev_signal_start(loop, &sig_watcher);

/* libuv */
static void sigint_cb(uv_signal_t *handle, int signum) {
    uv_signal_stop(handle);
    uv_stop(uv_default_loop());
}
uv_signal_t sig;
uv_signal_init(uv_default_loop(), &sig);
uv_signal_start(&sig, sigint_cb, SIGINT);

Installation on Linux

From Package Managers

# Debian/Ubuntu
sudo apt install libevent-dev libev-dev libuv1-dev

# Fedora/RHEL
sudo dnf install libevent-devel libev-devel libuv-devel

# Arch Linux
sudo pacman -s libevent libev libuv

Building from Source

# libevent
git clone https://github.com/libevent/libevent.git
cd libevent && mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc) && sudo make install

# libev
wget http://dist.schmorp.de/libev/libev-4.33.tar.gz
tar xzf libev-4.33.tar.gz && cd libev-4.33
./configure --prefix=/usr && make -j$(nproc) && sudo make install

# libuv
git clone https://github.com/libuv/libuv.git
cd libuv && mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc) && sudo make install

Real-World Usage

Notable Projects by Library

LibraryProjectUse Case
libeventMemcachedConnection handling
libeventTorNetwork I/O
libeventRedisNetworking (optional)
libeventChromiumAsync DNS
libevnginx (module)Event loop
libevShadow (sim)Network simulation
libevrxvt-unicodeTerminal emulator
libuvNode.jsCore event loop
libuvJuliaAsync I/O runtime
libuvCMakeFile watching
libuvNeovimEvent loop + process mgmt

Hybrid Approaches

Some projects combine libraries. For example, using libev for the event loop and libuv’s threadpool for filesystem operations:

/* Conceptual: ev_loop + libuv threadpool */
/* This requires careful integration; not trivial */

Summary

graph TD
    subgraph "libevent"
        LE["Full-featured<br>HTTP, DNS, SSL<br>~200KB"]
    end
    subgraph "libev"
        LV["Minimal &amp; fast<br>Pure event loop<br>~20KB"]
    end
    subgraph "libuv"
        LU["Complete platform<br>FS, DNS, threads<br>~300KB"]
    end
    LE ---|"Trade features<br>for speed"| LV
    LV ---|"Trade size<br>for features"| LU
    style LE fill:#3182ce,color:#fff
    style LV fill:#d69e2e,color:#000
    style LU fill:#38a169,color:#fff

All three libraries are mature, production-proven, and actively maintained. The choice boils down to:

  1. libev — you want speed and minimalism
  2. libevent — you want batteries included (HTTP, DNS, SSL)
  3. libuv — you need async I/O beyond networking (filesystem, processes, cross-platform)

For new projects on Linux, libuv is often the pragmatic default unless you need absolute minimal footprint. For embedded or performance-critical servers, libev remains the gold standard.