You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Roadmap v4.0: Target Actix-web performance parity and superiority #319
With CWIST v3.9 released, the primary performance objective for the upcoming v4.0 (first LTS / stable release) is to surpass Actix-web across throughput, mean latency, and tail latency (P99 / P99.999).
CWIST already consistently outperforms Axum, Gin, and Spring Boot under standard CI benchmarking profiles. However, Actix-web represents the highest-throughput compiled async web framework standard on Linux.
Action Items & Milestones
Benchmark Integration & Continuous Tracking
Integrated Actix-web (actix-web = 4) into the CI webserver benchmark matrix in .github/workflows/bsd-kqueue-benchmarks.yml.
Added Actix metrics to render_webserver_svg and KDE latency density chart generation (scripts/ci/benchmark.py).
Documented the measurement contract in docs/webserver-benchmark.md.
Core Reactor & I/O Optimizations for v4.0
Profile event loop overhead against Actix's thread-local worker model.
Maximize zero-copy socket buffer handoff and batch submission in the io_uring / epoll reactor.
Reduce memory allocation overhead in HTTP header parsing and path dispatching.
Investigate lock-free connection dispatching and worker CPU core pinning enhancements.
Verification
Continuously track Actix-web vs CWIST (Classic & C1M) in automated GitHub Actions runs across various runner hardware (AMD EPYC, Intel Xeon).
Target >= 1.05x Actix throughput and lower P99.999 tail latency across all runner architectures.
Source: the benchmarker latest run, side-by-side comparison. The site labels CWIST as "3.9"; the build under test is 3.9.1. Both frameworks come from the same run on the same hardware.
Latency in ms, lower is better. Each cell shows cwist / actix (Δ), where Δ is how much slower cwist is than actix (negative means cwist is faster).
Metric
64 conns
256 conns
512 conns
Min
0.05 / 0.07 (−29%)
0.05 / 0.04 (+25%)
0.05 / 0.06 (−17%)
Average
0.36 / 0.30 (+20%)
1.60 / 1.32 (+21%)
3.27 / 2.73 (+20%)
P50
0.37 / 0.30 (+23%)
1.60 / 1.32 (+21%)
3.34 / 2.73 (+22%)
P75
0.43 / 0.31 (+39%)
1.70 / 1.33 (+28%)
3.51 / 2.75 (+28%)
P90
0.48 / 0.32 (+50%)
1.81 / 1.35 (+34%)
3.62 / 2.77 (+31%)
P99
0.50 / 0.33 (+52%)
1.84 / 1.38 (+33%)
3.68 / 2.81 (+31%)
P99.999
1.17 / 1.06 (+10%)
7.28 / 5.15 (+41%)
8.48 / 9.82 (−14%)
Max
n/a
n/a
n/a
The throughput (req/s) table was not included in the export, so it is not shown here.
Observations
Median gap stays at about 20% at every concurrency level. Because the gap does not grow with load, it points to a fixed cost per request rather than a scaling problem.
cwist's latency spreads out more within the distribution. From P50 to P99, cwist goes 0.37 → 0.50 ms at 64 conns (+35%), while actix stays almost flat at 0.30 → 0.33 ms (+10%). The same pattern holds at 256 and 512 conns.
Extreme tail: cwist is behind at 256 conns (+41%) but ahead of actix at 512 conns (8.48 vs 9.82 ms).
Minimum latency is about the same for both, so the raw syscall path is not where cwist loses.
Next for #319: find the fixed per-request cost (dispatch, response build/serialize, write path) and the source of the P50–P99 spread (wakeup/batching jitter). Then rerun this comparison once the changes now on dev (async logger #370, late fast-path #361, syscall reduction #336) ship in a release.
Since v3.9.1, these PRs have landed on dev: #336 (syscall reduction), #349, #356, #361 (late fast-path) and #370 (async logger). This A/B checks whether any of them changes the workload the-benchmarker runs.
Setup
App: benchmarks/web-frameworks/main.c (the-benchmarker contract app), GET /, CWIST_WORKERS=4 as in the published config.yaml.
Both trees built the same way (make libcwist.a, app at -O2), each with the libttak commit it pins. BoringSSL and lsquic are shared between them.
Machine: Ryzen 5 5600X (6C/12T), Linux 6.12, loopback.
Load: wrk 10 s per run after a 3 s warm-up, fresh server process per run. v3.9.1 and dev runs are interleaved, with the order flipped every round. Every value below is the median across rounds.
Server-bound: server on 2 physical cores (cpus 0,1,6,7), wrk -t8 on the other 4
6 rounds per cell. Avg/P50/P99 latency in ms.
Conns
Metric
v3.9.1
dev
Δ
64
req/s
348.2k
345.8k
−0.7%
64
Avg / P50 / P99
0.18 / 0.17 / 0.45
0.19 / 0.17 / 0.52
+2.5% / −3.2% / +15.2%
256
req/s
366.2k
364.8k
−0.4%
256
Avg / P50 / P99
0.68 / 0.67 / 1.10
0.68 / 0.67 / 1.12
+0.4% / −0.7% / +1.4%
512
req/s
369.0k
367.0k
−0.5%
512
Avg / P50 / P99
1.35 / 1.34 / 1.97
1.35 / 1.35 / 1.97
+0.4% / +0.7% / 0.0%
Client-bound: server on 4 physical cores, wrk -t4 on 2
4 rounds per cell, plus 10 extra rounds at 512 conns.
Conns
v3.9.1 req/s
dev req/s
64
343.4k
342.3k
256
342.9k
343.0k
512 (14 runs)
342k
341.5k
Every cell sits at about 342k. That is wrk's ceiling with 2 cores, so this setup can't show a difference in server throughput.
Findings
No measurable change between v3.9.1 and dev on this workload. Throughput is within ±1% and P50 within noise. The PRs merged since v3.9.1 target syscalls on other paths, the async/deferred path and log output. The contract app hits none of these: its handlers are empty, it has no logger, and it never defers. So a new release alone should not move the published numbers.
dev's P99 at 64 conns varied more between runs: dev 0.42–0.86 ms vs v3.9.1 0.40–0.49 ms over 6 runs, about +15% at the median. It didn't show up at 256 or 512 conns. It is within noise for a shared desktop, but I'm noting it.
One open item. In the first client-bound pass, dev dropped to about 263k in 2 of 4 runs at 512 conns, and to 278k in 1 of 4 at 64 conns. 263k is about 3/4 of the 342k ceiling, which is what one idle worker out of 4 would look like. A rerun of 10 runs per version at 512 conns didn't reproduce it: dev ran 339–343k, v3.9.1 338–343k. I'm treating it as noise from the shared machine for now. It is worth watching per-worker accept counts in the next profiling pass.
Activity
docs(roadmap): add v4.0 readiness section
gg582 commentedon Oct 9, 2026
perf(reactor): lower the late-round default to 3 ms (#319 experiment)
Merge pull request #361 from c4punks/perf/late-fastpath
perf(reactor): late-round mode, an experiment for the tail (#319) (#361)
gg582 commentedon Oct 13, 2026
External benchmark: CWIST 3.9.1 vs actix-web 4.8
Source: the benchmarker latest run, side-by-side comparison. The site labels CWIST as "3.9"; the build under test is 3.9.1. Both frameworks come from the same run on the same hardware.
Latency in ms, lower is better. Each cell shows cwist / actix (Δ), where Δ is how much slower cwist is than actix (negative means cwist is faster).
The throughput (req/s) table was not included in the export, so it is not shown here.
Observations
Next for #319: find the fixed per-request cost (dispatch, response build/serialize, write path) and the source of the P50–P99 spread (wakeup/batching jitter). Then rerun this comparison once the changes now on dev (async logger #370, late fast-path #361, syscall reduction #336) ship in a release.
gg582 commentedon Oct 13, 2026
Local A/B: v3.9.1 vs dev (
f00cafda)Since v3.9.1, these PRs have landed on dev: #336 (syscall reduction), #349, #356, #361 (late fast-path) and #370 (async logger). This A/B checks whether any of them changes the workload the-benchmarker runs.
Setup
benchmarks/web-frameworks/main.c(the-benchmarker contract app),GET /,CWIST_WORKERS=4as in the publishedconfig.yaml.make libcwist.a, app at-O2), each with the libttak commit it pins. BoringSSL and lsquic are shared between them.Server-bound: server on 2 physical cores (cpus 0,1,6,7), wrk
-t8on the other 46 rounds per cell. Avg/P50/P99 latency in ms.
Client-bound: server on 4 physical cores, wrk
-t4on 24 rounds per cell, plus 10 extra rounds at 512 conns.
Every cell sits at about 342k. That is wrk's ceiling with 2 cores, so this setup can't show a difference in server throughput.
Findings
Next steps for #319
Since the release-to-release delta is flat, the about-20% median gap to actix has to come from the request path itself. Next steps:
perf recordonGET /(parse, route, response build/serialize, write) to find the fixed per-request cost.CWIST_WORKERS(4 / 6 / 8 / auto). The published config pins 4 workers, while actix uses every core on the benchmark host.strace -cto compare syscalls per request on keep-alive against actix.