C Web development Is Still Trustworthy
Write your routes and handlers once in C. Serve them from a native server, a WASI 0.2 edge runtime, a browser Service Worker, or bare-metal MCU firmware.
Bare-metal embedded support: raw-lwIP HTTP, optional mbedTLS HTTPS, static connection pools, and bounded response streaming on Cortex-M, validated on a physical STM32F769I-DISCO board.
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.
Bare-metal firmware uses cwist_app_dispatch_memory() or the raw-lwIP
adapter without BSD sockets, pthreads, epoll, or the hosted C1M reactor.
Dogfooded in production (maintainer-operated): oborona.zip (daily engineering blog) · fly.board
| Target | Runtime and transport | Validation and examples |
|---|---|---|
| Linux, macOS, BSD | HTTP/1.1, HTTP/2, HTTP/3, TLS, WebSocket, WebTransport, io_uring/epoll/kqueue | simple-server |
| WASI 0.2 and browser Service Worker | The same routes and handlers on Wasmtime or in the browser | wasip2-kv, wasm-service-worker |
| Bare-metal STM32 and Cortex-M | Raw-lwIP HTTP, optional mbedTLS HTTPS, static connection pools, bounded streamed responses | STM32 guide; STM32F769I-DISCO fortress (physical-board validated); NUCLEO-H743ZI2 example (reference integration: cross-build / QEMU) |
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 registers routes and middleware on a cwist_app. The same route
and handler definitions compile across four target environments, while transport
and runtime capabilities follow each 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 |
| Bare-metal STM32 firmware | cwist_app_dispatch_memory() or the raw-lwIP adapter |
Routing, middleware, JSON helpers, and HTTP/1.1 without BSD sockets or pthreads; optional mbedTLS HTTPS and bounded streaming | STM32 guide, fortress |
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 |
|---|---|
| Embedded / bare metal | Cortex-M Thumb-2 builds, STM32F769I-DISCO (physical-board validated) and NUCLEO-H743ZI2 (reference integration) examples, raw-lwIP HTTP, optional mbedTLS HTTPS, static connection pools, bounded response streaming |
| 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 completed CI benchmark (2026-11-06, commit b3834e0d, 4 vCPU AMD EPYC 9V74):
| CWIST reactor | CWIST Classic | Actix | Axum | Gin | Spring Boot | |
|---|---|---|---|---|---|---|
| req/s | 141,827 | 125,506 | 138,706 | 122,094 | 93,312 | 60,884 |
| Mean latency | 2.80 ms | 1.82 ms | 2.84 ms | 3.13 ms | 6.16 ms | 6.54 ms |
| p50 (median) | 2.60 ms | 1.43 ms | 2.63 ms | 2.93 ms | 4.05 ms | 5.95 ms |
| p99 | 8.20 ms | 6.91 ms | 7.74 ms | 7.91 ms | 38.17 ms | 14.25 ms |
| P99.999 | 18.15 ms | 25.76 ms | 15.87 ms | 15.65 ms | 84.59 ms | 59.71 ms |
| RSS | 22.2 MiB | 58.4 MiB | 12.0 MiB | 14.8 MiB | 27.5 MiB | 1.22 GiB |
| Context switches | 123,365 | 1,244,131 | 155,742 | 244,680 | 310,205 | 277,009 |
In this run the reactor is within runner variance of Actix on throughput (~2%
apart) with fewer 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 (Latency in that summary line is wrk mean
latency), 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.
On the async path, established HTTP/1.1 TLS connections are served on the C1M
reactor by default (CWIST_HTTPS_REACTOR=1). TLS handshakes and HTTP/2
requests continue to use HTTPS pool threads; setting CWIST_HTTPS_REACTOR=0
restores pool-based HTTP/1.1 handling. A handler that writes faster than the
socket drains waits inside the reactor thread (bounded by
CWIST_HTTP_TIMEOUT_MS). Idle cleartext keep-alive connections are closed by
a per-worker sweep timer once they wait longer than
CWIST_HTTP_KEEP_ALIVE_TIMEOUT (default 15 s), counted in
cwist_http_idle_reaped_total.
- 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 routes and handlers reach the browser, the edge, and MCUs. C compiles to WASM and bare-metal targets with mature toolchains, which lets shared handler logic run across the 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. libFuzzer + ASan/UBSan targets (
make fuzz) exercise the sequenced-chunk assembler (tests/fuzz_seq.c), HTTP/1.1 request/query/cookie/date/multipart parsers (tests/fuzz_http1.c), HTTP/2 HPACK and frame handling (tests/fuzz_http2.c), HTTP/3 QPACK header validation and QUIC packet-in paths (tests/fuzz_http3.c), WebSocket frame and UTF-8 parsing (tests/fuzz_websocket.c), and reverse-proxy URL/header/body sanitization (tests/fuzz_proxy.c). - 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. - Built-in security controls and opt-in PQC TLS. Default HTTPS enforces TLS
1.2+ with AEAD cipher suites (
X25519:P-256), whilecwist_app_use_pqc_layer(app, true)opts into hybrid post-quantum key exchange (X25519MLKEM768:X25519:P-256) and a TLS 1.3 minimum. Built-in middleware and session helpers provide WAF checks, 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.
CWIST also provides a socket-free Cortex-M path for firmware that uses
cwist_app_dispatch_memory() or the raw-lwIP adapter. The same routing,
middleware, handlers, JSON helpers, and HTTP/1.1 parser are available without
cwist_app_listen(), pthreads, epoll, or the C1M reactor.
Each board carries exactly one of three labels:
| Board | Status | What backs it |
|---|---|---|
| STM32F769I-DISCO | Physical-board validated | Flashed and run on real hardware (example/stm32f769, fortress) |
| NUCLEO-H743ZI2 | Reference integration (cross-build / QEMU) | Cross-built; not yet run on a physical board (example/stm32) |
| STM32F429I-DISCO | Planned | Low-resource target; hardware on hand, not validated yet |
- Validated on STM32F769I-DISCO: raw-lwIP HTTP, mbedTLS HTTPS, static connection pools, and streamed responses through bounded TX buffers.
- LCD and load demo:
example/stm32f769-fortress/exposes 288 real connection-pool slots on the 800x480 LCD and browser dashboard, with selectable responses up to 1 MiB. - Reference integration: NUCLEO-H743ZI2 application, HAL hooks, and lwIP options. It is cross-built only and has not been run on a physical board.
- Automated checks (not hardware validation):
baremetal.ymlbuilds Cortex-M7 Thumb-2 firmware witharm-none-eabi-gccand runs it underqemu-armsemihosting (make baremetal-smoke,make baremetal-size-gate);cross-compile.ymlcross-buildsfat-distfor embedded Linux triplets and link-smokes it under qemu-user. Passing either says nothing about a board's PHY, clock tree, NVIC, or RNG.
Build inputs and the lwIP/TLS integration are documented in docs/embedded/stm32.md. Board-specific wiring, cache constraints, and flashing instructions live in the example READMEs.
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: 114167 req/s | Latency 2.01ms (P50 1.65ms, P90 4.06ms, P99 7.26ms, P99.999 22.56ms) | RSS 60364KiB | Csw 1118408
- CWIST reactor: 142108 req/s | Latency 2.79ms (P50 2.57ms, P90 5.08ms, P99 7.99ms, P99.999 14.29ms) | RSS 22340KiB | Csw 124995
- Actix: 128508 req/s | Latency 3.05ms (P50 2.85ms, P90 5.05ms, P99 7.67ms, P99.999 15.36ms) | RSS 12384KiB | Csw 166962
- Axum: 109270 req/s | Latency 3.61ms (P50 3.41ms, P90 6.03ms, P99 8.89ms, P99.999 15.29ms) | RSS 16024KiB | Csw 252103
- Gin (Go): 78282 req/s | Latency 8.06ms (P50 4.88ms, P90 20.48ms, P99 52.06ms, P99.999 120.58ms) | RSS 27956KiB | Csw 329310
- Spring Boot: 44854 req/s | Latency 8.91ms (P50 8.19ms, P90 11.54ms, P99 19.76ms, P99.999 75.68ms) | RSS 1312444KiB | Csw 234822
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.5" 2026-10-20 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.
Maintainer-operated 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.
