Keel
An I/O core, TCP/UDP transports, and pluggable protocol stacks

One runtime. Every layer of the stack.

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.

339K req/s benchmark peak
Readiness & completion I/O
TCP · UDP · HTTP · WS · DNS
No forced dependencies
the flagship stack compilable C11
#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.

Build & test

One library. Two I/O models.

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.

shell local build
# 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
339K
req/s peak
Apple M1 Max, repo benchmark suite. Sub-millisecond p99.
863
tests
61 suites under ASan/UBSan, plus 8 fuzz targets.
8
event backends
epoll, kqueue, WSAPoll, poll, io_uring, IOCP, an lwIP raw callback provider (NO_SYS, no OS sockets), and a portable completion double.
0
forced deps
BYO allocator, TLS, parser, compression. One vendored parser by default.

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.

Architecture

Three layers, cleanly separated.

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.

Protocol stacks pluggable, above both axes
HTTP/1.1, HTTP/2, WebSocket (RFC 6455), Server-Sent Events, and a built-in async DNS resolver: dual A+AAAA, EDNS0, DNS cookies, TCP fallback, /etc/hosts. Sync and async HTTP clients, connection pooling, redirects, compression, and PROXY-protocol recovery ride the same core.
Transports first-class & symmetric
TCP and UDP as peers, not an afterthought. A datagram server symmetric with the TCP server (shared event loop), source/dest addressing, multicast join/leave, GSO/GRO segmentation offload, recvmmsg/sendmmsg batching, and ECN/DSCP marking.
I/O runtime a libuv-class foundation
An orthogonal event core: readiness (epoll/kqueue/WSAPoll/poll) and completion (io_uring/IOCP), over a platform-neutral socket seam (POSIX/Winsock). Single-threaded per worker, with timers, async connection suspension, thread-pool offload, backpressure, and zero-copy splice file sends. Scale horizontally with SO_REUSEPORT.
Principles

Small pieces. Explicit edges.

Orthogonal axes

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.

No forced buffering

Buffer, file, and streaming response modes let services avoid copying when sendfile, writev, chunking, or backpressure are the right tool.

Batteries optional

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.

Compare

One runtime, not three libraries.

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.

Protocols & maturity

What's shipped, and how solid it is.

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.
Performance

Fast because there's nothing in the way.

No GC pauses, no async runtime, no goroutine scheduler. Just acceptreadwrite 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.

339K req/s
peak throughput
GET /hello, Apple M1 Max, repo bench suite.
<1 ms
p99 latency
Sub-millisecond tail at 100 connections.
~2 %
feature overhead
Route params + middleware + body reading vs baseline.
benchmark wrk, 4t / 100c / 10s
$ 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%
What's next

The architecture predicts the roadmap.

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.

QUIC / HTTP-3

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.

MQTT

A TCP pub/sub protocol that drops straight onto the existing connection core as a peer to the HTTP stack. No runtime changes needed.

Alternate net stacks

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.

How it works

Event loop first. Protocols around it.

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.

Two I/O models, one API

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.

Suspend and resume

Async handlers can suspend a connection and resume it from a file descriptor watcher, timer, client callback, or worker completion.

Body readers

Request bodies are routed into explicit readers: buffer, multipart, stream-oriented custom parsers, or application-specific sinks.

Streaming edges

Chunked responses, SSE, WebSocket frames, backpressure drain buffers, and client pools keep long-lived connections cheap.

Security model

A narrow surface, built to be constrained.

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.

Resource limits at the edge

Header size, body size, multipart part limits, drain buffers, connection timeouts, idle timers, and route-level readers make overload behavior explicit.

Fuzzed protocol surfaces

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.

Bring your own hardening

TLS, allocators, DNS, compression, file IO, and parsers are vtables, so production users can choose the backend appropriate for their threat model.

Trust

Built like infrastructure.

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.

quality gates repo workflow
$ 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.