Architecting Zero-Dependency Web Apps in Modern C: Linux epoll, Arena Allocators, and Zero-Copy Routing
Ditch the heavy runtimes and microservice bloat to build lightning-fast web applications in pure C. Learn how to architect high-throughput HTTP servers using Linux epoll, arena allocators, and zero-copy string views.
The Case for Bare-Metal Web Services
Modern backend architectures have normalized multi-gigabyte container footprints, complex dependency graphs, and multi-tier runtimes for tasks that boil down to parsing an HTTP request, querying data, and serializing a response. While JavaScript, Python, and Go offer rapid scaffolding, they come at the cost of non-deterministic garbage collection pauses, opaque runtime overhead, and significant CPU cache pollution.
Building a web application in modern C (C11/C23) is often dismissed as masochistic. However, with modern idioms—specifically arena-based memory allocation, non-blocking asynchronous event multiplexing via epoll, and structured pointer-length string slices—writing web services in pure C is not only practical, but yields orders-of-magnitude improvements in throughput, latency predictability, and resource density. An entire HTTP API service can run inside a statically linked 300KB binary, consuming less than 8MB of RAM while handling 100,000+ requests per second.
Here is how to engineer a production-ready, zero-dependency web service in C from the ground up.
The Event Core: Asynchronous Multiplexing with epoll
Thread-per-connection architectures buckle under thousands of concurrent open sockets due to context-switching overhead and thread stack consumption. Instead, modern Linux network programming leverages edge-triggered epoll to drive an asynchronous, single-threaded or thread-per-core event loop.
To construct this core, initialize a non-blocking TCP server socket and register it with an epoll instance:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAX_EVENTS 1024
#define PORT 8080
int make_socket_non_blocking(int sfd) {
int flags = fcntl(sfd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(sfd, F_SETFL, flags | O_NONBLOCK);
}
int create_and_bind(int port) {
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd == -1) return -1;
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = INADDR_ANY
};
if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
close(sfd);
return -1;
}
return sfd;
}
When configuring epoll_ctl, utilizing EPOLLET (edge-triggered mode) guarantees that notifications are delivered only on state transitions, reducing wake-up overhead. The server loop monitors client file descriptors, drains incoming buffers completely until EAGAIN or EWOULDBLOCK is returned, and transitions the state machine.
Deterministic Memory: Per-Request Arena Allocators
The primary vector for vulnerabilities and performance degradation in C is manual, dynamic heap allocation via malloc and free. In high-throughput network programming, dynamic heap operations result in cache misses and memory fragmentation.
The solution is linear arena allocation. Because HTTP request lifecycles are strictly ephemeral, each connection or worker thread can be assigned a contiguous chunk of memory (e.g., 64KB). Every string allocation, header map, or intermediate payload allocates monotonically by bumping an offset pointer. When the response is dispatched, the offset resets to zero. Freeing memory becomes an $O(1)$ operation without any risk of memory leaks or double frees.
typedef struct {
char *buffer;
size_t capacity;
size_t offset;
} MemoryArena;
MemoryArena arena_create(size_t capacity) {
return (MemoryArena){
.buffer = (char *)malloc(capacity),
.capacity = capacity,
.offset = 0
};
}
void *arena_alloc(MemoryArena *arena, size_t size) {
// Align to 8-byte boundary
size_t aligned_size = (size + 7) & ~7;
if (arena->offset + aligned_size > arena->capacity) {
return NULL; // Out of memory for this request context
}
void *ptr = &arena->buffer[arena->offset];
arena->offset += aligned_size;
return ptr;
}
void arena_reset(MemoryArena *arena) {
arena->offset = 0;
}
This simple pattern eliminates 95% of conventional C memory bugs while providing instantaneous allocation speeds.
Zero-Copy HTTP Request Parsing
Traditional web stacks allocate new string objects for every HTTP method, URL path, and header key-value pair. In pure C, we can parse HTTP/1.1 requests without allocating a single byte of new heap memory by using string slices (pointers to points in the raw read buffer paired with lengths).
typedef struct {
const char *ptr;
size_t len;
} StringView;
typedef struct {
StringView key;
StringView value;
} Header;
typedef struct {
StringView method;
StringView path;
int minor_version;
Header headers[32];
size_t header_count;
StringView body;
} HttpRequest;
A state machine processes the incoming request buffer byte-by-byte, advancing the state while recording offsets:
- Read the HTTP method (
GET,POST,PUT,DELETE). - Extract the URI path up to the query string separator (
?) or whitespace. - Scan line-by-line for header pairs separated by
:. - Detect the end of headers signaled by the
\r\n\r\nsequence. - Map the remaining bytes to the
bodyfield according toContent-Length.
Because the strings are represented as non-null-terminated StringView primitives, you avoid calling strcpy, strtok, or allocating intermediate memory. When route matching is executed, comparison operations rely on SIMD-friendly bounded comparisons such as memcmp(route, req.path.ptr, req.path.len).
Zero-Copy Response Dispatching via writev
Constructing an HTTP response typically involves assembling the status line, standard headers (Content-Type, Content-Length), and the payload. Instead of copying all components into a massive dynamically sized buffer with snprintf, POSIX vectored I/O (writev) lets you scatter-gather buffers directly across the socket.
#include <sys/uio.h>
void send_json_response(int fd, int status_code, const char *status_text, const char *json_body, size_t body_len) {
char header_buf[256];
int header_len = snprintf(header_buf, sizeof(header_buf),
"HTTP/1.1 %d %s\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"Connection: keep-alive\r\n\r\n",
status_code, status_text, body_len);
struct iovec iov[2];
iov[0].iov_base = header_buf;
iov[0].iov_len = header_len;
iov[1].iov_base = (void *)json_body;
iov[1].iov_len = body_len;
// Atomically write both headers and payload in a single syscall
writev(fd, iov, 2);
}
Using writev minimizes system call context switches, eliminates buffer concatenation overhead, and ensures packets are populated with maximum efficiency under TCP nodelay.
Hardening the Service: Linux Namespaces and Seccomp
Writing web apps in C naturally introduces security concerns regarding buffer overflows and memory corruption. However, modern deployment environments provide tools to isolate C binaries effectively:
- Compile with Hardened Toolchains: Always build with
-Wall -Wextra -Wpedantic -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pieto enable stack canaries, address space layout randomization (ASLR), and runtime bounds checking for standard functions. - Linux Seccomp-BPF: Restrict the process to only the syscalls it fundamentally needs (
read,write,writev,epoll_wait,epoll_ctl,close,exit_group). Even in the event of an exploit, the process cannot invokeexecve, spawn shells, or access unmapped files. - Run as Unprivileged Nobody: Drop all capabilities (
setuid,setgid) immediately after binding to the privileged port.
Conclusion: The Mechanical Advantage
Writing a web application in modern C requires disciplined architectural boundaries. By replacing runtime garbage collectors with per-request arena allocators, swapping multi-threading sync locks with edge-triggered non-blocking epoll, and adopting zero-copy pointer views, you build systems that operate with absolute mechanical sympathy.
The result is a web application that boots in sub-millisecond time, consumes negligible memory, and delivers deterministic microsecond response latencies under enterprise-grade loads.