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 & 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
| Feature | libevent | libev | libuv |
|---|---|---|---|
| First release | 2000 | 2007 | 2012 |
| Language | C | C | C |
| License | BSD-3-Clause | BSD-2-Clause | MIT |
| Size (compiled) | ~200KB | ~20KB | ~300KB |
| Dependencies | OpenSSL (optional) | None | None |
| Backend | epoll/kqueue/poll | epoll/kqueue/poll | epoll/kqueue/IOCP |
| Timers | Min-heap | Min-heap + list | Min-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 users | Memcached, Tor | nginx (module), | Node.js, Julia, |
| Redis, Chromium | libev, Shadow | Luv (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
| Library | Project | Use Case |
|---|---|---|
| libevent | Memcached | Connection handling |
| libevent | Tor | Network I/O |
| libevent | Redis | Networking (optional) |
| libevent | Chromium | Async DNS |
| libev | nginx (module) | Event loop |
| libev | Shadow (sim) | Network simulation |
| libev | rxvt-unicode | Terminal emulator |
| libuv | Node.js | Core event loop |
| libuv | Julia | Async I/O runtime |
| libuv | CMake | File watching |
| libuv | Neovim | Event 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 & 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:
- libev — you want speed and minimalism
- libevent — you want batteries included (HTTP, DNS, SSL)
- 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.