Keel is a portable, dependency-free C11 networking runtime: an async I/O core (readiness and completion), TCP and UDP transports, and a growing family of pluggable protocol stacks. HTTP is the flagship, not the definition.
#include <keel/keel.h> static void handle_run(KlRequest *req, KlResponse *res, void *ctx) { (void)req; (void)ctx; static const char body[] = "{\"ok\":true,\"runtime\":\"keel\"}"; kl_response_json(res, 200, body, sizeof(body) - 1); } int main(void) { KlServer s; KlConfig cfg = { .port = 7070 }; kl_server_init(&s, &cfg); kl_server_route(&s, "GET", "/run", handle_run, NULL, NULL); kl_server_run(&s); kl_server_free(&s); }
The HTTP server is one protocol stack riding the runtime. The same event loop, socket seam, and transports carry the UDP datagram server, the WebSocket and SSE stacks, and Keel's own from-scratch async DNS resolver. Swap the event backend without changing a line of protocol code.
Keel builds as a static C library. Pick a readiness engine (epoll, kqueue, WSAPoll, poll) or a completion engine (io_uring, IOCP) at build time; the protocol stacks above never change.
# Build libkeel.a with the platform default (epoll / kqueue). $ make # Linux 5.6+: completion-native I/O over io_uring (SQE/CQE, splice). $ make BACKEND=iouring # Universal POSIX fallback; also enables Cosmopolitan APE builds. $ make BACKEND=poll # Run the test suite and example programs. $ make test $ make examples
Benchmark context: make bench runs a dedicated local benchmark server across four endpoints with wrk latency reporting. Defaults are 4 threads, 100 connections, and 10s duration; the benchmark workflow also runs Linux epoll, Linux io_uring, and macOS kqueue on CI.
Keel is not an HTTP library with an event loop bolted on. It is an I/O runtime with protocols on top, and the axes stay orthogonal: the event model and the socket implementation are independent, and every protocol sits above both without knowing which is in use.
The tell is DNS: Keel ships its own async resolver, built from scratch on the UDP layer. A runtime that already hosts a non-HTTP protocol is a runtime, not a web server.
/etc/hosts. Sync and
async HTTP clients, connection pooling, redirects, compression, and
PROXY-protocol recovery ride the same core.
recvmmsg/sendmmsg
batching, and ECN/DSCP marking.
splice file sends.
Scale horizontally with SO_REUSEPORT.
The event model and the socket implementation are independent axes; allocator, parser, body reader, TLS, resolver, and compression are replaceable vtables. An audit confirmed protocol code contains no platform event or socket logic.
Buffer, file, and streaming response modes let services avoid copying when sendfile, writev, chunking, or backpressure are the right tool.
No mandatory OpenSSL, no forced runtime. Bring your own allocator, TLS, parser, and compression backend, or use the defaults. One vendored parser (llhttp), 2-second clean builds.
A typical C stack assembles an I/O loop, an HTTP server, and a DNS resolver from separate projects with separate build systems and dependency trees. Keel unifies that surface under one orthogonal, dependency-free architecture.
| Capability | Keel | libuv | h2o | c-ares |
|---|---|---|---|---|
| Async I/O loop: readiness and completion | yes | yes | via libuv | no |
| TCP and UDP transports | yes | yes | tcp | no |
| HTTP/1.1 + HTTP/2 stack | yes | no | yes | no |
| WebSocket & SSE | yes | no | ws | no |
| Built-in async DNS resolver | yes | sync | no | yes |
| Dependency-free (BYO TLS/parser/alloc) | yes | yes | no | yes |
| Portable incl. Windows & Cosmopolitan | yes | yes | part | yes |
A rough map, not a scoreboard: each of these projects is excellent at its job. The point is architectural: Keel spans the I/O core (libuv), the HTTP stack (h2o), and DNS (c-ares) behind one seam, so an alternate event engine or socket stack drops in below every layer at once.
An honest status per surface. The I/O core, transports, and the HTTP/1.1 + WebSocket + SSE + DNS stacks are the mature center; HTTP/2 and TLS are deliberately pluggable so you choose the backend and threat model.
| Surface | Status | Notes |
|---|---|---|
| I/O event core | stable | Readiness (epoll, kqueue, WSAPoll, poll) and completion (io_uring, IOCP) behind one small API over a POSIX/Winsock socket seam; an orthogonality audit confirms the axes stay independent. |
| Timers & worker wakeups | stable | One-shot timers, async connection suspend/resume, thread-pool offload, and cross-thread event-loop wakeup. |
| TCP server & client | stable | Connection pool + state machine, keep-alive, timeouts, backpressure drain; blocking and async clients with pooling and redirects. |
| UDP datagram server | stable | Symmetric with the TCP server on a shared loop; source/dest addressing, multicast, GSO/GRO, recvmmsg/sendmmsg batching, ECN/DSCP. |
| HTTP/1.1 | stable | Routing, middleware, keep-alive, buffered/file/stream responses; server and client covered by the main test and example suite. |
| WebSocket | tested | RFC 6455 server and client, shared frame parser, masked client frames, dedicated fuzz corpus. |
| Server-Sent Events | stable | Zero-alloc SSE framing over chunked streaming responses. |
| Async DNS resolver | built-in | From-scratch resolver over UDP: dual A+AAAA (RFC 8305), EDNS0, DNS cookies, resolv.conf search/ndots, TCP fallback; response parser is fuzzed. Pluggable vtable + caching decorator too. |
| Compression / decompression | stable | Pluggable vtables; miniz gzip/deflate backend ships in-tree. |
| HTTP proxy / PROXY protocol | stable | Client HTTP forwarding + HTTPS tunneling; PROXY v1/v2 parsing with CIDR trust to recover the real client behind an L4 balancer. |
| HTTP/2 | pluggable | Server and client surfaces use session vtables; backend choice (e.g. nghttp2) stays explicit. |
| TLS | bring your own | Keel defines the transport vtable and avoids vendoring TLS policy; mTLS peer-cert identity is surfaced. |
| Socket provider | pluggable | Capability-gated vtable (POSIX and Winsock built in; lwIP shipped — both BSD-socket and a raw NO_SYS stack; AF_XDP-ready), selectable per server or client. |
No GC pauses, no async runtime, no goroutine scheduler. Just
accept →
read →
write with zero-copy pointers into
read buffers and splice/sendfile
on the file path. Route params, middleware, and body reading land
within ~2% of the baseline.
On the completion axis, the io_uring backend runs the same platform-independent driver as IOCP, measured 2 to 2.3× faster than the retired readiness-io_uring adapter. epoll remains the safe default on Linux; io_uring is one build flag away.
$ make bench # GET /hello peak throughput, sub-ms p99 # GET /users/:id route params: within ~2% of baseline # GET /mw/hello middleware chain: within ~2% # POST /echo body reading: within ~2%
Because protocols ride the runtime and the axes stay orthogonal, the next steps are additive: a new stack on the existing core, or a new provider below every stack, never a rewrite.
A protocol stack over UDP + TLS. The async UDP datagram layer, the completion backends, and the TLS vtable already exist, so HTTP/3 is "another stack," exactly like HTTP/2 was. Prerequisite: UDP pktinfo/GSO/GRO parity on the completion path.
A TCP pub/sub protocol that drops straight onto the existing connection core as a peer to the HTTP stack. No runtime changes needed.
Already proven: the full stack — server, client, UDP, DNS, and
HTTPS — runs on a stock libkeel.a over a bare
lwIP TCP/IP stack with no OS sockets
(NO_SYS raw callbacks). AF_XDP, DPDK, or a
UEFI firmware provider slot in the same way,
below every protocol stack, and none of them know. An
architectural capability HTTP libraries simply don't have.
A composable event context owns the loop, watchers, timers, and allocator state. Server, client, thread pool, SSE, WebSocket, UDP, and the DNS resolver all sit on that same loop.
Readiness (epoll/kqueue/WSAPoll/poll) and completion (io_uring/IOCP) expose the same small contract to higher layers; the same generic driver runs across every completion backend.
Async handlers can suspend a connection and resume it from a file descriptor watcher, timer, client callback, or worker completion.
Request bodies are routed into explicit readers: buffer, multipart, stream-oriented custom parsers, or application-specific sinks.
Chunked responses, SSE, WebSocket frames, backpressure drain buffers, and client pools keep long-lived connections cheap.
Keel keeps the byte-moving path small enough to test, fuzz, bound, and place behind kernel sandboxing. The init/run split and explicit resource limits make lockdown natural.
Header size, body size, multipart part limits, drain buffers, connection timeouts, idle timers, and route-level readers make overload behavior explicit.
Eight libFuzzer targets cover the untrusted-input attack surface: the HTTP parser, response parser, multipart, WebSocket, DNS responses, PROXY protocol, and URL parsing, all under ASan/UBSan.
TLS, allocators, DNS, compression, file IO, and parsers are vtables, so production users can choose the backend appropriate for their threat model.
Keel is intentionally boring in the places that matter: C11, static library, a wide CI matrix, sanitizers, fuzzing, static analysis, and examples that compile against the public API.
$ make debug # ASan + UBSan $ make test # 863 tests across 56 suites $ make analyze # clang static analyzer $ make cppcheck # static analysis $ make fuzz # 8 parser and protocol fuzz targets
CI runs the suite across Linux (epoll, poll, musl), macOS (kqueue), Windows (WSAPoll + IOCP), the io_uring completion backend, and Cosmopolitan APE builds.