Build an HTTP service
HTTP/1.1, routing and WebSockets; optional HTTP/2 and TLS adapters.
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.
#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.
HTTP/1.1, routing and WebSockets; optional HTTP/2 and TLS adapters.
KlEventCtx, KlListener, KlStream and KlDatagram: bounded buffering and explicit lifetimes.
Unix-domain sockets, Windows Named Pipes and directional process pipes. Your application owns process management.
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.
HTTP/1.1 · HTTP/2 · WebSocket · SSE · DNS
KlListener · KlStream · KlDatagram
Accept ownership · bounded writes · message boundaries · confirmed close
Connect semantics to compatible execution and handle operations
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.
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.
Clocks, entropy, threads, wakeups and native resource operations. PAL is supporting machinery, not one universal I/O stage.
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.
/etc/hosts. Sync and
async HTTP clients, connection pooling, redirects, compression, and
PROXY-protocol recovery ride the same core.
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.
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.
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.
# 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.
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.
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.
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.
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 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 / 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.
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 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.
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.
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.
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.
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.
| 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. |
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.
$ 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
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.
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.
These links show integration boundaries in public source code, not deployment or production-use guarantees.
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.
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.
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.
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.
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. 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.
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.
TLS, allocators, DNS, compression, file IO, and parsers are vtables, so teams 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-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.