Keel
C11 · asynchronous transports · protocol stacks

One event loop. Every transport under it.

A portable, asynchronous transport and protocol substrate written in C11. Shared transport contracts across readiness and completion engines, native sockets and local IPC, with HTTP, WebSocket and DNS built on top.

Transport semantics stay separate from event execution, socket providers and platform machinery. Build a service or embed the runtime without putting backend mechanics in your protocol code. Support depends on the transport, engine and platform.

Keel v3.3.0: anonymous pipes for process I/O, writable-again stream callbacks, and six rounds of audit hardening.

Readiness & completion I/O
TCP · UDP · Unix sockets · Pipes
No forced dependencies
the flagship stack compilable C11
#include <keel/keel.h>

static void handle_run(KlHttpRequest *req,
                       KlHttpResponse *res,
                       void *ctx) {
    (void)req;
    (void)ctx;

    static const char body[] =
        "{\"ok\":true,\"runtime\":\"keel\"}";
    kl_http_response_json(res, 200, body,
        sizeof(body) - 1);
}

int main(void) {
    KlHttpServer s;
    KlHttpServerConfig cfg = { .port = 7070 };

    if (kl_http_server_init(&s, &cfg) != 0)
        return 1;
    if (kl_http_server_route(&s, "GET", "/run",
            handle_run, NULL, NULL) != 0) {
        kl_http_server_free(&s);
        return 1;
    }
    int rc = kl_http_server_run(&s);
    kl_http_server_free(&s);
    return rc != 0;
}

HTTP is one consumer of the runtime; the transport contracts also serve networking and local IPC. Build and run hello_server for a first request.

One library. Three ways in.

Architecture

Separate meaning from machinery.

Keel separates what a transport means from how its I/O executes. Protocols use transport contracts. Drivers connect those contracts to compatible event engines and handle-specific operations. The event axis and the socket-provider axis are independent; a socket provider is not a universal native-handle provider.

The built-in DNS resolver uses the datagram transport. Pipes use the stream contract without entering the socket-provider seam. Platform support is negotiated, not an all-combinations promise.

Living architecture docs: overview, invariants, public API.

Application protocols

HTTP/1.1 · HTTP/2 · WebSocket · SSE · DNS

Semantic transport contracts

KlListener · KlStream · KlDatagram

Accept ownership · bounded writes · message boundaries · confirmed close

Transport drivers and adapters

Connect semantics to compatible execution and handle operations

Event execution axis

Readiness
epoll · kqueue · WSAPoll · poll

Register interest, perform I/O, retry on would-block

Completion
io_uring · IOCP

Submit owned operations, complete and retire them

pollcomp is a completion test double. Native pipe support depends on the engine.

Handle-specific paths

Socket-backed drivers
TCP · UDP · Unix-domain sockets

KlSocketProvider: POSIX / Winsock / lwIP / EFI, within each provider's capabilities

Non-socket pipe drivers
Named Pipes · anonymous pipes

Native pipe operations; no KlSocketProvider. Windows: IOCP. POSIX anonymous pipes: readiness via native-descriptor watchers, including completion-engine relays.

Platform services alongside the drivers and engines

Clocks, entropy, threads, wakeups and native resource operations. PAL is supporting machinery, not one universal I/O stage.

Transport semantics above; independent execution and handle machinery below.

Protocols stay above backend mechanics. Socket drivers use both an event engine and a socket provider. Pipe drivers share the transport contracts and use native pipe operations instead of the socket provider. Only compatible combinations are supported; see the support table below.

Protocol and runtime detail
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 peers on one loop
Byte streams (KlStream), listeners (KlListener), and datagrams (KlDatagram) are peers on the same loop, not an afterthought. Named pipes and anonymous process-I/O pipes expose the same stream contract, including writable-again backpressure callbacks. KlDatagram exposes async receive with source and destination addressing; batching (recvmmsg/sendmmsg), GSO/GRO offload, multicast join/leave, and per-packet ECN/DSCP are capability-gated extensions, available where the platform and provider support them.
I/O runtime readiness + completion core
A composable event context (KlEventCtx) over an orthogonal event core: readiness (epoll/kqueue/WSAPoll/poll) and completion (io_uring/IOCP), on 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.
First use

A running server in a few commands.

On Linux or macOS, use a C11 compiler, GNU make and a POSIX shell. Git checks out the source; curl checks the response. The default backend is epoll on Linux and kqueue on macOS. No TLS or HTTP/2 dependency is needed for this example.

shell local build
# Get the v3.3.0 source release.
$ git clone --branch v3.3.0 --depth 1 https://github.com/artalis-io/keel.git
$ cd keel

# Build libkeel.a and the existing example.
$ make examples/hello_server
$ ./examples/hello_server 8080

# In a second terminal:
$ curl http://localhost:8080/hello
{"msg":"hello"}
# Stop the server with Ctrl+C.

Example source · Build instructions · Windows / MSVC setup. Installation, static linking and backend prerequisites are in the platform guide.

Keel v3.3.0

Process I/O on the same loop. Hardened across the stack.

Anonymous pipe pairs extend the stream transport to an embedder's child processes. Six rounds of C audits and their re-reviews tighten parsing, callback lifetime, TLS, DNS, HTTP/2, and completion I/O. Read the release and upgrade notes.

Anonymous pipe pairs

kl_anon_pipe_create returns a directional KlPipeStream and a native child end for your process spawner. POSIX engines use ordinary pipes through watchers; Windows uses private named pipes on IOCP. Keel leaves process creation to the embedder. Both ends start close-on-exec or non-inheritable.

Pipe API and platform limits, native-end handoff.

Writable-again callbacks

After KL_STREAM_WOULD_BLOCK, kl_stream_on_writable signals when to retry, on sockets and pipes alike. The retry reports whether capacity returned or the write side failed. Graceful close also completes after a terminal write failure.

Stream and close contract.

Limits and lifetime safety

Bounded chunk extensions and trailers, deferred async-client startup errors, TLS-aware pool APIs, and an optional redirect policy callback. DNS spoofing resistance, HTTP/2 limits and h2c upgrade handling, and ordered, bounded completion output receive fixes across the stack.

Upgrading from 3.2.0: rebuild all consumers against the new headers; Keel preserves source compatibility, not ABI compatibility. Build requirements are unchanged. Review the observable behavior changes: HTTP/2 client bodies default to a 16 MiB per-stream cap; mbedTLS client creation without a CA bundle now fails (unverified clients use an explicit insecure factory); and enum switches must account for KL_ERR_REDIRECT_REFUSED and KL_IO_RESOURCE_EXHAUSTED.

Evidence

Every backend maps to a named gate.

Evidence you can inspect: Linux, macOS and Windows CI; readiness and completion unit suites under ASan/UBSan; native MinGW and MSVC validation; fuzzing and structural architecture gates.

Unit suites, sanitizer smokes, strict compile gates and local firmware harnesses establish different evidence. Passing tests do not prove security; the matrix keeps those distinctions visible.

The full matrix lives in docs/operations/capability_matrix.md; the gate inventory in testing.md.

Backend and platform coverage: execution model and the exact gate that exercises each.
Backend / platform Model How it is exercised
epoll (Linux) readiness Unit + integration suites and example smokes on the Linux (epoll) CI job; ASan/UBSan via make debug-test.
kqueue (macOS) readiness Unit + integration suites and example smokes on the macOS (kqueue) CI job.
poll (POSIX fallback) readiness Unit suites on the Linux (poll fallback) CI job; the Cosmopolitan build auto-selects it.
WSAPoll (Windows) readiness TCP, datagram, and DNS smokes plus a unit subset on the Windows (MinGW / Winsock) CI job. Built and run again by a second native toolchain on the Windows (native MSVC, cl.exe) job, whose suite set is derived from the MinGW one rather than curated.
io_uring (Linux 5.6+) completion Completion unit coverage is machine-checked: pollcomp derives its eligible suite set automatically, while io_uring’s enrolled set (make test-iouring) is parity-checked against the sanitizer lane. There are currently no completion exclusions. Plus HTTP and async smokes under ASan/UBSan on the io_uring CI jobs.
IOCP (Windows) completion Unit + integration suites via make test-win-iocp, plus HTTP, mock-TLS, async/thread-pool, and datagram smokes on the Windows (IOCP) CI job. Native MSVC also runs IOCP runtime probes via make CC=cl BACKEND=iocp test-msvc.
pollcomp (portable double) completion HTTP / TLS / WebSocket / async smokes, a stream-contract oracle, ASan/UBSan, and runtime-injection on the completion (poll) CI job.
lwIP raw (NO_SYS) completion provider Runtime loopback smokes on CI: loopback-raw and loopback-raw-asan (the latter under ASan/UBSan/LSan) for the raw provider; loopback and loopback-tls for the lwIP BSD-socket provider. make integration-lwip is the provider compile gate only.
UEFI EFI_TCP4/UDP4 completion provider A strict freestanding-PE compile gate on CI (make uefi-dgram-gate UEFI_GATE_STRICT=1, x86_64 + aarch64, -Werror -Wpedantic). The host-mock unit tests and the end-to-end QEMU/OVMF self-tests are local-only harnesses, not hosted CI.

Benchmarks are reproducible rather than headline figures. make bench runs a local benchmark server across four endpoints with wrk latency reporting (defaults: 4 threads, 100 connections, 10s). Sample results with their environment are recorded in the README; numbers depend on hardware, backend, and build, so run it yourself rather than trusting a single quoted peak.

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.

Unified

One seam under every layer.

A C service is often assembled from a separate event loop, HTTP server, and DNS resolver, each with its own build system and dependency tree. Keel puts the I/O core, the transports, the protocol families, and its own resolver behind a single socket-and-event seam. Swap the event model or the socket provider once, below everything.

One I/O core

Readiness and completion behind one API, over a POSIX/Winsock socket seam. A new event backend or socket provider is a drop-in below every transport and protocol at once, verified by the axis audit.

Streams and datagrams

KlStream, KlListener, and KlDatagram are peers on the same loop. Socket streams, named pipes, and anonymous pipe pairs share the stream contract; TCP and UDP protocols use the same runtime.

Protocols on top

HTTP/1.1, WebSocket, SSE, and a from-scratch async DNS resolver ship in-tree; HTTP/2 and TLS are pluggable vtables. None of them include a platform networking header or touch an event engine directly.

This is a description of Keel's own composition, not a scoreboard against other projects. The load-bearing claim is architectural: the axes stay orthogonal, so an alternate event engine or socket stack drops in below every layer at once. See docs/architecture/invariants.md.

Protocols & maturity

What's shipped, per surface.

A factual status per surface. The I/O core, transports, and the HTTP/1.1 + WebSocket + SSE + DNS stacks are built in; HTTP/2 and TLS are pluggable vtables so you choose the backend and threat model.

Frozen contracts: stream, datagram, streaming, async lifecycle.

Shipped surfaces: status and notes per protocol and transport surface.
Surface Status Notes
I/O event core built-in 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 built-in One-shot timers, async connection suspend/resume, thread-pool offload, and cross-thread event-loop wakeup.
TCP server & client built-in Connection pool + state machine, keep-alive, timeouts, backpressure drain; blocking and async clients with pooling and redirects. TLS-aware pool acquire/release APIs and an optional redirect policy callback.
Unix-domain sockets built-in Local socket transport, including an HTTP server example and peer credentials where supported. Windows filesystem-node lifecycle is limited to capable systems on local NTFS; other volumes are refused. This is a socket-provider path, distinct from Named Pipes.
Windows Named Pipes built-in Local-IPC client and listener, each connection an ordinary KlStream: the Windows counterpart of an AF_UNIX stream. IOCP engine only (refused, not emulated, on readiness engines); never routed through the socket seam.
Anonymous pipe pairs built-in kl_anon_pipe_create: a read-only or write-only KlPipeStream plus a native child end. POSIX engines with native-descriptor watchers (including io_uring/pollcomp); Windows IOCP only. Both ends start close-on-exec/non-inheritable; the embedder owns spawning and child-end handoff. Cosmopolitan on a Windows host has a known blocked-writer busy-wake defect.
Stream backpressure built-in kl_stream_on_writable delivers a writable-again edge after KL_STREAM_WOULD_BLOCK. Retry the write to learn whether capacity returned or the write side failed; works across socket and pipe streams.
Datagram transport built-in KlDatagram on the shared event loop: async receive with source and destination addressing. Batching (recvmmsg/sendmmsg), GSO/GRO offload, multicast, and per-packet ECN/DSCP are capability-gated, provider-dependent extensions.
HTTP/1.1 built-in 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 built-in 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 optional extension Pluggable vtables; miniz gzip/deflate backend ships in-tree.
HTTP proxy / PROXY protocol built-in 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. h2c upgrade support is an optional server-session op. Client response bodies default to a 16 MiB per-stream cap; server stream, window, and header-list limits reach the adapter.
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 socket vtable. POSIX and Winsock built in; lwIP ships BSD-socket and raw NO_SYS providers; UEFI uses EFI_TCP4/UDP4 tokens. Raw lwIP and EFI do not provide native pipe handles or promise Unix sockets. Raw lwIP runtime smokes run in CI; UEFI has strict compile gates and local-only firmware tests. Provider/engine combinations must be compatible.
Performance

Bounded paths you can measure.

Each connection runs a bounded path: accept → read → write, with zero-copy pointers into read buffers, splice/sendfile on the file path, and recvmmsg/sendmmsg batching on the datagram path where the platform supports it. The benchmark endpoints below measure what routing, middleware, and body reading add over a bare baseline; run make bench to measure the delta on your own hardware.

epoll is the default on Linux; io_uring is the completion-native path one build flag away (make BACKEND=iouring), and runs the same protocol code. Which is faster depends on workload and kernel, so the honest answer is to measure both.

baseline
GET /hello: minimal JSON, no routing params, no middleware. The floor everything else is measured against.
feature cost
Route params, a middleware chain, and body reading measured as a delta over that baseline, not an absolute claim.
reproducible
One command, one local server, wrk latency reporting. Documented sample numbers with their environment live in the README.
benchmark wrk, 4t / 100c / 10s
$ make bench

# GET /hello        baseline: minimal JSON
# GET /users/:id    router: param extraction
# GET /mw/hello     middleware: 2 pass-through layers
# POST /echo        body reading: buffer + echo

Used in public code

Hull

Runtime networking and async backends wrap Keel's HTTP server, request suspension, event loop, timers, watchers and worker pool. Its Windows path uses Cosmopolitan; this is not a claim of native IOCP pipe use.

Networking adapter · Async adapter

OTTO

Native API-server helpers use Keel HTTP responses, asynchronous request handling and SSE. The helpers are compiled by API servers; they are deliberately kept out of the shared library and its WASM builds.

HTTP helpers · Async and SSE helpers

These links show integration boundaries in public source code, not deployment or production-use guarantees.

What's next

Extensions follow real consumers.

New transports, engines and protocols are added when real consumers demonstrate a need and the feature fits the architectural boundaries. QUIC/HTTP-3, MQTT and packet-I/O providers below are possible extensions, without delivery dates. Shipped providers and documented limitations are listed separately in the support table and repository roadmap.

QUIC / HTTP-3
possible

Not shipped or scheduled. A future QUIC integration would need its own transport and TLS requirements verified against the datagram capabilities, including completion-path metadata and offload support. Existing UDP and TLS seams alone do not establish QUIC support.

MQTT
possible

Not shipped or scheduled. A pub/sub protocol could build on the stream contract if a consumer needs it. Its framing, session lifetime and backpressure would need their own implementation and verification.

Alternate net stacks
shipped + possible

Shipped: the stack (server, client, UDP, DNS, HTTPS) runs on a stock libkeel.a over a bare lwIP TCP/IP stack (NO_SYS raw callbacks), and a stock async client runs inside UEFI firmware over EFI_TCP4/UDP4 tokens (plaintext, HTTPS with CA / hostname / validity-time checks, and DNS), exercised on QEMU/OVMF. Possible: AF_XDP or DPDK providers would need capability and lifecycle validation. Neither is shipped or scheduled here.

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. Chunked requests cap extension lists at 4096 bytes and trailer sections at 8192 bytes, rejecting bare LF and disallowed control bytes. HTTP/2 clients default to a 16 MiB body cap per stream.

Fuzzed protocol surfaces

libFuzzer targets exercise untrusted protocol input: the HTTP parser, response parser, multipart, WebSocket, DNS responses, PROXY protocol, and URL parsing, all under ASan/UBSan; an optional gzip/deflate decompression target builds on demand. See docs/operations/fuzzing.md.

Bring your own hardening

TLS, allocators, DNS, compression, file IO, and parsers are vtables, so teams 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-test # unit + integration suites under ASan/UBSan
$ make analyze    # clang static analyzer
$ make cppcheck   # static analysis
$ make fuzz       # libFuzzer parser and protocol targets
$ make check-pipe-seam check-cloexec # pipe isolation and descriptor inheritance
$ make check-no-em-dash check-no-milestones # text-hygiene gates

CI runs the suite across Linux (epoll, poll fallback, and a no-completion build), macOS (kqueue), the pollcomp and io_uring completion backends, and Windows on two native toolchains (MinGW and MSVC cl.exe, over WSAPoll and IOCP). The per-backend gates are enumerated in the testing and capability-matrix docs.