|
cwist
Modern C web framework with libttak runtime
|
C Web development Is Still Trustworthy
Write a web app once in C. Serve it from a native server, from a WASI 0.2 edge runtime, or from inside the browser's Service Worker.
CWIST is a C17 web framework and application server: routing, middleware, templates, sessions, validation and an embedded SQLite ORM on top of HTTP/1.1, HTTP/2, HTTP/3, WebSocket and WebTransport, with an io_uring/epoll/kqueue reactor.
Powered by CWIST in production: oborona.zip (daily engineering blog) · fly.board
Or scaffold a hot-reloading project with cwist new project demo && cwist watcher. Next steps: the 30 modules in tutorials/ and the guides in docs/.
A CWIST app is a set of routes on a cwist_app. Where it runs depends only on how you hand that app to a host:
| Host | Entry point | What you get | Example |
|---|---|---|---|
| Native server (Linux, macOS, BSD) | cwist_app_listen(app, port) | The full stack: HTTP/1.1–3, TLS, WebSocket, WebTransport, C1M reactor | simple-server |
| Browser Service Worker | CWIST_WASM_DEFINE_ENTRY(app), built with emcc | Routing, zod validation, templates, cwist_db and signed-cookie sessions answering fetch inside the browser: no origin server, no framework JS | wasm-service-worker |
| WASI 0.2 edge runtime (Wasmtime) | cwist_app_listen on WASI sockets, built for wasm32-wasip2 | The same app on Wasmtime; the host owns persistence as a KV-style blob | wasip2-kv |
The WASM hosts speak serialized HTTP/1.1 across a WIT interface, and the cwist-wasm npm package wraps the jco-transpiled component for JavaScript hosts. They are newer than the native server: read each example's README for what is and is not guaranteed yet (for example, the wasip2 demo does not promise crash-durable writes).
| Layer | What you get |
|---|---|
| Protocols | HTTP/1.1, HTTP/2 (h2/h2c), HTTP/3 (QUIC, via lsquic), WebSocket, WebTransport, WebRTC data channels |
| TLS / Security | BoringSSL, one-call hybrid post-quantum TLS (X25519MLKEM768), ECH, certificate hot reload, JWT, CSRF, WAF, rate limiting, secure headers, encrypted DB columns |
| Database | SQLite3 + ORM and migrations, Nuke DB (in-memory SQLite synced to disk on COMMIT), PostgreSQL/MySQL/MariaDB detection by wire protocol |
| Routing | Express-style :param routes, mux router, chainable middleware |
| Async handlers | cwist_async_defer / cwist_async_respond: move blocking work to a job thread and answer later without stalling the reactor |
| gRPC / Protobuf | Unary and streaming routes, health/reflection, cwist proto code generation, and a channel client with load balancing and gRFC A6 retries |
| Rendering | HTML builder, scoped-CSS components, template engine, JSON builder |
| Observability | Structured access logs, metrics endpoint, healthz |
| Tooling | cwist CLI: scaffolding, incremental hot reload, OpenAPI 3.1 from Doxygen annotations, proto3 codegen |
| Bindings | Rust and Zig |
Every push and pull request runs CWIST against Actix, Axum, Gin and Spring Boot under wrk -t12 -c400 -d10s on a GitHub runner.
Latest run at the time of writing (2026-10-20, commit 94937494, 4 vCPU AMD EPYC 9V74):
| CWIST reactor | CWIST Classic | Actix | Axum | Gin | Spring Boot | |
|---|---|---|---|---|---|---|
| req/s | 183,303 | 155,941 | 174,662 | 151,695 | 116,080 | 74,789 |
| p50 | 1.95 ms | 1.21 ms | 2.04 ms | 2.38 ms | 3.31 ms | 4.98 ms |
| p99 | 7.57 ms | 5.92 ms | 7.08 ms | 6.92 ms | 21.34 ms | 10.37 ms |
| P99.999 | 22.01 ms | 18.63 ms | 13.55 ms | 14.29 ms | 43.04 ms | 42.51 ms |
| RSS | 21.5 MiB | 59.7 MiB | 11.7 MiB | 14.6 MiB | 30.1 MiB | 1.25 GiB |
| Context switches | 145,617 | 1,488,999 | 189,420 | 260,600 | 367,969 | 338,919 |
In this run the reactor has the highest throughput and the fewest context switches, and the Classic pool answers the median request in about half of Axum's time and has the lowest p99. Actix and Axum have the tighter extreme tail (P99.999) and the smaller memory footprint; closing the tail gap is open work. The benchmark section below is regenerated by CI, so it shows the most recent run, which may be newer than this table. Methodology and fairness settings are in docs/webserver-benchmark.md.
The mode is one environment variable; pick per workload.
CWIST_C1M_MODE unset or 1) takes throughput and connection density. Many connections share each event loop, so a held connection costs a reactor slot (about 5 KB over TLS) instead of a parked thread. Start here for APIs, web pages, webhooks, SSE fan-out, WebSocket chat and anything that holds many mostly idle connections.CWIST_C1M_MODE=0) takes median latency. Each connection gets its own thread, so no request waits behind another in a batch. Pick it for short request bursts where the median matters more than connection count; it cannot reach C1M, because a million threads exceeds threads-max.Measured on one 12-core machine (Ryzen 5 5600X, 62 GB, Linux 6.12, loopback):
| Run | Result | Server memory |
|---|---|---|
| C1M cleartext, reactor, 12 workers (2026-09-29) | 1,000,000 / 1,000,000 responded | ~22.9 GiB peak RSS (~24 KB per connection) |
| C100K HTTPS, reactor, 4 workers (2026-10-17) | 100,000 / 100,000 established, 0 dropped, all answered | 4,975 B per open connection |
| C1M HTTPS, reactor (2026-10-18) | 1,000,000 / 1,000,000 established, 0 dropped, all answered | 4.56 GiB above idle (4,866 B per open connection) |
| C1M over TLS HTTP/1.1 and HTTP/2 via fly.board | 1,000,000 / 1,000,000 held and served, zero failures | 19.9 GB / 30.1 GB PSS |
The HTTPS ramp is handshake-bound (~2,900 full P-256 handshakes/s, so ~340 s to open 1M connections). Kernel prerequisites, the client tooling fixes the runs needed, and the per-run details are in docs/performance/https-c100k.md and docs/c1m-file-limits.md.
Known limits of the async path: HTTPS still runs the request phase on pool threads, a handler that writes faster than the socket drains waits inside the reactor thread (bounded by CWIST_HTTP_TIMEOUT_MS), and idle cleartext keep-alive connections are not yet reaped by a timer.
The first question about a C web server is buffer overflows, use-after-free and leaks. What CWIST does about it, and where each check lives:
SANITIZE=address,undefined WERROR=1 and run under ASan/UBSan (asan.yml); ThreadSanitizer, CodeQL and cppcheck run in their own workflows.scripts/ci/h2spec-baseline.txt); regressions fail the build.malloc/free calls in the framework fail cwist audit --gate against scripts/ci/malloc-baseline.txt (malloc_lint.sh).make fuzz_seq, tests/fuzz_seq.c). Fuzz targets for the HTTP/1.1, HTTP/2, WebSocket and multipart parsers are not written yet, and contributions are welcome.CWIST_ASYNC_STRICT=1 turns misuse into an abort instead of a warning.Design decisions and their current status are recorded in docs/decisions/.
CWIST vendors its dependencies (BoringSSL, lsquic, libttak, SQLite3), so a plain build works on a fresh Linux, macOS, or BSD machine:
macOS and Linux (Linuxbrew) users can install the latest release from the CWIST tap:
The formula builds from the release source tarball (dist/cwist-<version>.tar.gz, vendored dependencies included) and installs libcwist.a, public headers, the cwist CLI, and cwist.pc pkg-config metadata under the Homebrew prefix.
CWIST builds on Linux, macOS, and BSD. On Linux the reactor uses io_uring (raw syscalls, no liburing) and falls back to epoll if ring setup fails; macOS and BSD use kqueue. The HTTP/3 server and native client use non-blocking UDP sockets on every platform, and ECN metadata is enabled only when the host exposes the required socket options. Reactor internals (one-shot polls vs completion-staged receives, CWIST_RX_URING, latency probes) are described in docs/http-reactor-fairness.md and docs/runtime-config.md.
make install also installs the cwist command line tool into $(PREFIX)/bin.
The watcher rebuilds only the affected translation units and restarts the app only after a successful build, so the previous process keeps serving if a build fails. It wakes on inotify (Linux) or kqueue (BSD/macOS) with a polling fallback; --no-run rebuilds without running and --poll forces polling. The .cwpro manifest (cwist-project/v1) holds the entry point, include paths, debounce/stop timeouts and route discovery scope for the watcher and generators.
libcwist.a is a thin static archive: it contains only CWIST objects. make install installs the vendored libraries under $(PREFIX)/lib/cwist and their headers under $(PREFIX)/include/cwist/vendor. pkg-config resolves all of it:
Without pkg-config, link CWIST first and its dependencies after it:
-lnghttp2 (HTTP/2) and -lcurl (RDBMS auto-mount probing) are optional. Brotli ships as libbrotli-dev on Debian/Ubuntu and brotli-devel on Fedora/RHEL; zstd as libzstd-dev / libzstd-devel. DESTDIR is supported for staged packages.
CWIST reads .env files and prefixed environment variables:
The framework itself reads these without a prefix; every CWIST_* variable, with its default and v4 support status, is in docs/runtime-config.md.
| Variable | Default | Description |
|---|---|---|
CWIST_WORKERS | 1 | Worker processes to fork before entering the event loop. |
CWIST_C1M_MODE | true | 1 selects the CWIST reactor, 0 the Classic pool. |
CWIST_HTTP_BATCH | 16 | Pipelined HTTP/1.1 requests dispatched per event-loop turn (clamped to [1, 1024]). |
CWIST_HTTP_YIELD_BATCH | 16 | Dispatch yield granularity within a batch before re-posting the connection. |
CWIST_ASYNC_DEBUG | unset | Log rare reactor failure events (rearm/submit/SQ failures) to stderr. |
CWIST_ASYNC_STRICT | unset | 1 aborts on deferred-response misuse instead of warning once. |
PQC TLS. cwist_app_use_pqc_layer(app, true) forces X25519MLKEM768:X25519:P-256, sets TLS 1.3 as the minimum and strips legacy ciphers. Application code never touches OpenSSL directly.
WebTransport. cwist_app_use_webtransport(app, handler) handles the CONNECT negotiation over HTTP/3 and exposes cwist_webtransport_read/write/flush/close/open_bidi/open_uni.
Nuke DB. cwist_nuke_init("data.db", 5000) loads an on-disk SQLite file into memory with sqlite3_deserialize, runs PRAGMA integrity_check, serves every query from RAM and syncs to disk on COMMIT. If bootstrap fails, it falls back to read-only disk protection mode.
RDBMS auto-mount. cwist_app_auto_rdbms(app, 5432) classifies the server by wire protocol (a PostgreSQL StartupMessage or a MySQL handshake packet) instead of guessing from the port number.
libttak. The in-tree allocator/reactor toolkit provides generational arenas, epoch-based reclamation, signal-safe detachable arenas for TLS and WebSocket buffers, and a lock-free job queue.
CI regenerates this block from each benchmark run.
Latest Web Server Benchmark (wrk -t12 -c400 -d10s (after 10s warmup, warmup discarded)):
Settled CWIST-internal tuning variants (glibc arena cap from PR #35, cooperative drain chunk from issue #25) are recorded in docs/cooperative-queuing.md and benchmarks/webserver.json; they are no longer rendered as comparison rows.
Spring runtime environment
openjdk version "25.0.4.1" 2026-08-18 LTSJVM options
Warmup/profile
wrk -t12 -c400 -d10s (after 10s warmup, warmup discarded)
Latency distribution (density curve reconstructed from each server's percentiles - shows the shape of the tail, not just its P99.999 number):
Methodology, JVM options, and fairness settings: docs/webserver-benchmark.md
dev and may appear or disappear between tags; pin an exact tag in production. See docs/versioning.md.-Werror on every push, and an h2spec HTTP/2 diff against a pinned baseline; known gaps are tracked in scripts/ci/h2spec-baseline.txt.The full map is docs/README.md. In suggested reading order:
main.c, a CMakeLists.txt and a guided README.docs/api/, the flat quick reference and generated Doxygen HTML.Runnable demos under example/: a minimal server, step-by-step HTTP, SQLite and encrypted-column DB, JWT auth, a WebSocket Othello game, WebRTC, WebTransport, the WASM hosts (Service Worker, wasip2), Rust and Zig hello-worlds, and the rps-showcase throughput demo. The examples table lists them all.
Production deployments built on CWIST: oborona.zip (live daily engineering blog) and fly.board.
The official CWIST Discord: https://discord.gg/6F8HDmNAPg. Questions, design discussion and contribution coordination happen there; see CONTRIBUTING.md to send a patch.
CWIST vendors its dependencies under lib/; each keeps its own license (summary in NOTICE.md, authoritative text in each submodule's license file):
| Component | License |
|---|---|
| BoringSSL | Apache-2.0 |
| lsquic | MIT (some Chromium-derived parts BSD-3-Clause) |
| cJSON, multipart-parser-c | MIT |
| libttak | BSD-3-Clause |
| SQLite | Public Domain |
| cnats | Apache-2.0 |
| uriparser | BSD-3-Clause (library only; its test suite is LGPL-2.1-or-later and is not linked) |
Static linking propagates each component's license obligations to linked binaries; review NOTICE.md when distributing.