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
brew tap c4punks/cwist
brew install c4punks/cwist/cwist#include <cwist/app.h>
static void hello(cwist_http_request *req, cwist_http_response *res) {
(void)req;
cwist_sstring_assign(res->body, "Hello from CWIST!");
}
int main(void) {
cwist_app *app = cwist_app_create();
cwist_app_get(app, "/", hello);
cwist_app_listen(app, 8080);
cwist_app_destroy(app);
return 0;
}gcc -o server main.c $(pkg-config --cflags --libs cwist)
./serverOr 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 both the Emscripten module
and 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 reactor (default,
CWIST_C1M_MODEunset or1) 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 Classic pool (
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 exceedsthreads-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.
- Your C and C++ code is already the backend. Libraries for finance, game servers, ML inference or device control link straight into the server with no FFI layer, no serialization boundary and no second runtime.
- The same app reaches the browser and the edge. C compiles to WASM with mature toolchains, which is what lets one CWIST app run in all three hosts above.
- Predictable footprint and start-up. No garbage collector, VM or JIT warm-up: the reactor's RSS is above the Rust frameworks but below Go and about 60× below the JVM.
- Competitive throughput and median latency, with the tail trade-off stated in the table above rather than hidden.
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:
- Sanitizers on every push and PR. The test suite, tutorials and examples
build with
SANITIZE=address,undefined WERROR=1and run under ASan/UBSan (asan.yml); ThreadSanitizer, CodeQL and cppcheck run in their own workflows. - Protocol conformance. An h2spec HTTP/2 run is diffed against a pinned
baseline (
scripts/ci/h2spec-baseline.txt); regressions fail the build. - Allocator gate. New raw
malloc/freecalls in the framework failcwist audit --gateagainstscripts/ci/malloc-baseline.txt(malloc_lint.sh). - Fuzzing. A libFuzzer target covers the sequenced-chunk assembler
(
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. - Deterministic lifetimes. Request buffers and cached replies live in
libttak generational arenas and epoch-based reclamation critical sections.
Deferred responses use an explicit ownership handoff, and
CWIST_ASYNC_STRICT=1turns misuse into an abort instead of a warning. - Secure defaults. Hybrid post-quantum TLS with TLS 1.3 minimum, a built-in WAF, token-bucket rate limiting, timing-safe HMAC session verification, CSRF tokens and strict security headers (HSTS, COOP, Permissions-Policy).
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:
git clone https://github.com/c4punks/CWIST.git
cd cwist
make
sudo make install # optional, installs to /usr/local (override with PREFIX=/opt/cwist)macOS and Linux (Linuxbrew) users can install the latest release from the CWIST tap:
brew tap c4punks/cwist
brew install cwistThe 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.
# Scaffold a project: src/main.c, Makefile, and a demo.cwpro manifest
cwist new project demo --directory ~/src
cd ~/src/demo
# Develop with hot reload (incremental rebuild + zero-downtime restart)
cwist watcher
# Generate OpenAPI 3.1 from Doxygen @openapi.* route annotations
cwist openapi
# Generate C models and gRPC method paths from proto3 definitions
cwist proto api.proto
# Inspect the project manifest and detected routes
cwist describeThe 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:
gcc -o server main.c $(pkg-config --cflags --libs cwist)
# fully static:
gcc -o server main.c $(pkg-config --cflags --libs --static cwist)Without pkg-config, link CWIST first and its dependencies after it:
gcc -I/opt/cwist/include -I/opt/cwist/include/cwist/vendor main.c \
-L/opt/cwist/lib -L/opt/cwist/lib/cwist -lcwist \
-llsquic -lssl -lcrypto -lnats_static -lttak -lcjson -luriparser \
-lz -lzstd -lbrotlienc -lbrotlicommon -ldl -lpthread -lm -lstdc++-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:
cwist_config *cfg = cwist_config_create();
cwist_config_load_file(cfg, ".env");
cwist_config_load_env(cfg, "CWIST_");
const char *db_url = cwist_config_get(cfg, "DATABASE_URL");
int workers = cwist_config_get_int(cfg, "WORKERS", 4);
bool debug = cwist_config_get_bool(cfg, "DEBUG", false);
cwist_config_destroy(cfg);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)):
- CWIST Classic pool: 110436 req/s | Latency 2.11ms (P90 4.38ms, P99 7.88ms, P99.999 21.94ms) | RSS 59368KiB | Csw 1082879
- CWIST reactor: 140383 req/s | Latency 2.82ms (P90 5.16ms, P99 8.12ms, P99.999 15.95ms) | RSS 21780KiB | Csw 108386
- Actix: 124523 req/s | Latency 3.15ms (P90 5.36ms, P99 7.83ms, P99.999 13.89ms) | RSS 12044KiB | Csw 155664
- Axum: 108355 req/s | Latency 3.63ms (P90 6.10ms, P99 9.10ms, P99.999 17.59ms) | RSS 16256KiB | Csw 187063
- Gin (Go): 75100 req/s | Latency 7.52ms (P90 18.71ms, P99 42.23ms, P99.999 90.43ms) | RSS 29648KiB | Csw 319085
- Spring Boot: 43118 req/s | Latency 9.18ms (P90 12.00ms, P99 18.79ms, P99.999 67.33ms) | RSS 1342008KiB | Csw 220820
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
- JDK:
openjdk version "25.0.4.1" 2026-08-18 LTS - Spring Boot: 3.2.3
- Stack: Spring WebFlux + Reactor Netty on native epoll (G1GC, JDK 25 Leyden AOT, virtual threads disabled)
- Virtual threads: disabled
JVM options
-Xms1024m
-Xmx1024m
-XX:+UseG1GC
-XX:GCTimeRatio=99
-XX:G1HeapRegionSize=1m
-XX:+AlwaysPreTouch
-XX:CompileThreshold=1500
-XX:CICompilerCount=4
-Djava.security.egd=file:/dev/urandom
-Djava.net.preferIPv4Stack=true
-Dio.netty.allocator.type=pooled
-Dio.netty.leakDetection.level=disabled
-Dio.netty.buffer.checkBounds=false
-Dio.netty.buffer.checkAccessible=false
-Dreactor.netty.ioWorkerCount=4
-Xlog:gc*:file=/tmp/spring_gc.log:time,uptime,level,tags
-XX:+AOTClassLinking
-XX:AOTCache=/tmp/spring_bench/app.aot (JEP 483 + JEP 514 single-step AOT)
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
- Versioning: v4 is the first LTS line. Draft-level features cycle through
devand may appear or disappear between tags; pin an exact tag in production. See docs/versioning.md. - Conformance gates: ASan/UBSan with
-Werroron every push, and an h2spec HTTP/2 diff against a pinned baseline; known gaps are tracked inscripts/ci/h2spec-baseline.txt.
The full map is docs/README.md. In suggested reading order:
- Tutorials: 30 hands-on modules, each with a runnable
main.c, aCMakeLists.txtand a guided README. - Guides: CRUD blog, NATS integration, WebTransport server.
- API reference: per-module docs under
docs/api/, the flat quick reference and generated Doxygen HTML. - Decision records and ROADMAP.md.
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.
