Benchmarks
Each result has its date and machine. It also lists cells where Wisp is not ahead. The native TechEmpower tables are on the home page and in bench/tfb/RESULTS.md.
This site shows the numbers of the newest release only. In the Wisp repo, bench/suites.tsv names each suite and its summary file. That file names the release and the commit it measured. When a new release is out, each suite not measured at it shows "Not measured at X yet." in place of its numbers, until it is measured again. The stamps step of cargo run -q -p wisp-gate checks the stamps and the pages.
git clone https://code.wispweb.dev/wispdev/wisp
cd wisp
cargo run -r -p bench-run -- --paths fortunes
You need Rust. bench/README.md lists every option and the frameworks compared. It also says how the harness checks each path for identical output. Compare frameworks only from the same machine.
The harnesses (bench-run, rank, native-rank, edge, rust-fastest) check each server's body against fixed answers before they time it. None ranks a cell with a failed request (a non-200 or a socket error). None ranks a run without a steal reading or with steal over the bar. An error page served fast never ranks. Throughput is the median of the runs (the mean of the middle two for an even count), with min..max beside it. The kept run of a redone one is the last, never the best.
Native, Linux
Source: bench/tfb/RESULTS.md (generated from results.json). The tables use the TechEmpower plaintext and JSON tests. Our script follows the method of the TechEmpower wrk scripts. The machine is a WSL2 VM with 12 vCPUs and 15.5 GB on a desktop PC (AMD Ryzen 7 7800X3D). The server runs on CPUs 4 and 6 and wrk on CPUs 1, 2, 8 and 10. They share the L3 cache and the memory bus of the host. Plaintext (pipeline depth 16) at 256, 1,024, 4,096 and 16,384 connections, JSON at 16, 32, 64, 128, 256 and 512. 5 runs of 15 s each in one session, the median of the valid runs. A cell gets a rank only from 5 valid runs. A cell with a client CPU at 95% or more is a lower bound and gets no rank. The valid runs are in bench/tfb/results.json. The raw wrk output stays on the machine that ran it. The release is v1.0.0-pre.3 (tag 0d2d15a03). The run used tag bench-pre3-h2 (95c716ac5), which has the product code of that release. This is not an official TechEmpower result. Overlaps of the min-max ranges are ties.
The kernel is 6.8.12-bench68, built from the Ubuntu source linux-source-6.8.0 with one patch: io_uring/kbuf.c line 552, an inverted check that made IORING_REGISTER_PBUF_RING fail. On this kernel Wisp registers buffer rings. On a kernel that refuses them Wisp lends buffers by PROVIDE_BUFFERS. That mode is not measured here, and the stock Ubuntu binary kernel is not checked. Wisp v1.0.0-pre.3 can hang on the stock WSL2 kernel 6.18, so this run does not use that kernel.
wrk has 4 CPUs because 2 limit it. In separate checks (Wisp 881724bda, after v1.0.0-pre.3) at JSON 64, 256 and 512 connections with wrk on 2 CPUs, the wrk CPUs were at 99 to 100%. Wisp reached medians of 289,559 to 292,367 with its server CPUs at 70 to 83%. Actix Web with 2 workers reached medians of 308,149 to 311,746. So at 2 wrk CPUs Actix Web is ahead of Wisp, and wrk limits both. The table below shows 4 wrk CPUs, where wrk limits neither. The number of wrk threads changes the rate of Actix Web: at 256 connections it reached 253,873 with 4 wrk CPUs in a separate run, 281,262 with 3 and 311,746 with 2. The cause is not proven.
Actix Web sets its workers to the CPUs it can use. This is a change to the TechEmpower source, because actix-server 2.2.0 uses num_cpus::get_physical(), which ignores taskset.
Wisp against Actix Web, requests a second, median (min-max), no failed request in either:
| Cell | Wisp | Actix Web |
|---|---|---|
| plaintext, 256 connections | 4,202,276 (4,074,733-4,286,248) | 1,773,586 (1,761,859-1,789,080) |
| plaintext, 1,024 connections | 4,129,701 (4,069,306-4,164,053) | 1,753,616 (1,736,349-1,773,433) |
| plaintext, 4,096 connections | 3,109,809 (3,003,151-3,121,421) | 1,674,872 (1,653,457-1,687,683) |
| plaintext, 16,384 connections | 1,847,760 (1,831,501-1,869,285) | 1,665,969 (1,644,969-1,680,444) |
| JSON, 16 connections | 335,701 (329,767-343,575) | 245,569 (238,648-249,673) |
| JSON, 32 connections | 377,967 (373,888-385,399) | 260,386 (256,579-264,747) |
| JSON, 64 connections | 418,494 (410,002-421,214) | 260,653 (257,125-265,430) |
| JSON, 128 connections | 446,152 (445,765-472,030) | 258,370 (254,423-261,640) |
| JSON, 256 connections | 450,239 (444,429-462,635) | 253,544 (248,001-257,058) |
| JSON, 512 connections | 460,034 (452,525-472,207) | 250,995 (246,189-254,207) |
Wisp's range is above Actix Web's in all ten cells.
Plaintext, pipelined, requests per second (median of the valid runs), by connections:
| Contender | 256 | 1024 | 4096 | 16384 |
|---|---|---|---|---|
| Wisp | 4,202,276 | 4,129,701 | 3,109,809 | 1,847,760 |
| Axum | 1,205,927 | 1,197,229 | 1,190,818 | 1,102,918 |
| Actix Web | 1,773,586 | 1,753,616 | 1,674,872 | 1,665,969 |
| Express | 120,912 | 121,999 | 120,171 | 115,539 |
| Fastify | 176,034 | 175,354 | 170,289 | 169,560 |
| Hono on Node.js | 112,019 | 111,597 | 112,070 | 108,710 |
| Hono on Bun | 227,026 | 229,660 | 227,063 | 229,415 |
| SvelteKit | 33,042 | 29,642 | 28,314 | 29,188 |
| Next.js | 10,155 | 10,837 | 10,902 | 10,593 |
| Nuxt | 92,976 | 80,006 | 63,905 | 61,056 |
Rows whose min-max ranges overlap are ties. .
JSON, requests per second (median of the valid runs), by connections:
| Contender | 16 | 32 | 64 | 128 | 256 | 512 |
|---|---|---|---|---|---|---|
| Wisp | 335,701 | 377,967 | 418,494 | 446,152 | 450,239 | 460,034 |
| Axum | 220,485 | 227,601 | 228,588 | 227,753 | 226,414 | 224,362 |
| Actix Web | 245,569 | 260,386 | 260,653 | 258,370 | 253,544 | 250,995 |
| Express | 78,168 | 80,687 | 80,284 | 79,328 | 78,981 | 78,990 |
| Fastify | 96,306 | 99,949 | 99,848 | 98,411 | 99,823 | 98,052 |
| Hono on Node.js | 59,387 | 59,993 | 59,581 | 59,281 | 58,579 | 58,510 |
| Hono on Bun | 169,558 | 181,642 | 194,523 | 188,479 | 213,293 | 204,491 |
| SvelteKit | 27,724 | 28,563 | 28,817 | 28,327 | 28,728 | 28,945 |
| Next.js | 5,042 | 5,062 | 5,013 | 5,020 | 4,950 | 4,951 |
| Nuxt | 73,296 | 81,650 | 85,775 | 79,622 | 80,690 | 79,967 |
Rows whose min-max ranges overlap are ties: 32: Express with Nuxt. 128: Express with Nuxt. 256: Express with Nuxt. 512: Express with Nuxt.
What these numbers come from:
- Driver fast path. The epoll and io_uring drivers receive, answer and send in their own turn without waking the connection's task, and for routes the build found never wait,
http::on_driveranswers without polling the future at all. - One pass over the request. The request line, headers and a JSON body are read in as few passes as possible, borrowed from the receive buffer instead of copied.
- Pooled buffers. A connection holds buffers only while it has a request. Idle ones go back to a pool per thread.
- No cost for what is off. HTTP/2 (feature
h2) is noticed only where HTTP/1 already refused the bytes, so HTTP/1 pays nothing. Features a route does not use add no instructions, checked by the A/B described in What Is Checked below.
Twenty Servers, Closed Loop, Linux
Source: bench/README.md ("Results on Linux") in the Wisp repo. The command is cargo run -r -p bench-run -- --rounds 6 on an Intel Xeon E3-1231 v3 (4 cores, 32 GB), kernel 7.0.0-38-generic. The run data records no commit of the tree. Each server uses 2 cores and the load generator the 2 others. 64 connections, closed loop, req/s the median of 6 rounds, steal 0.0% in every measurement, no failed request. CPU is server microseconds per request.
| Server | /plaintext | /json | /fortunes | /page | CPU on /page (us) |
|---|---|---|---|---|---|
| may-minihttp | 296,388 | 296,149 | 237,925 | 198,464 | 10.1 |
| Wisp | 289,884 | 289,916 | 269,747 | 230,145 | 8.7 |
| ntex | 271,107 | 267,648 | 229,074 | 108,650 | 18.3 |
| hyper | 245,328 | 243,830 | 209,516 | 179,413 | 11.2 |
| uWebSockets.js | 241,553 | 220,582 | 111,654 | 43,877 | 45.6 |
| xitca-web | 233,004 | 229,059 | 197,782 | 182,095 | 11.0 |
| Actix Web | 226,029 | 220,625 | 191,296 | 177,471 | 11.5 |
| Vert.x | 221,567 | 221,033 | 125,027 | 104,106 | 19.0 |
| Elysia | 211,822 | 202,521 | 103,604 | 45,336 | 44.1 |
| fasthttp | 207,983 | 185,313 | 42,076 | 8,761 | 225.8 |
| Fiber | 199,147 | 179,819 | 40,936 | 8,560 | 231.7 |
| Bun | 192,437 | 202,387 | 118,199 | 50,245 | 39.9 |
| Axum | 189,834 | 188,386 | 162,750 | 141,288 | 14.3 |
| ASP.NET Core | 122,149 | 121,513 | 49,488 | 30,268 | 65.7 |
| Gin | 104,254 | 97,773 | 33,080 | 7,826 | 251.9 |
| Go net/http | 104,226 | 97,067 | 32,915 | 7,812 | 252.2 |
| Fastify | 59,351 | 56,344 | 40,440 | 25,894 | 77.2 |
| Express | 33,033 | 31,385 | 26,192 | 18,654 | 107.2 |
| SvelteKit | 18,316 | 16,546 | 4,603 | 3,258 | 614.7 |
| Next.js | 3,516 | 3,846 | 856 | 567 | 3511.3 |
Wisp is first on /fortunes: 269,747, range 265,423-270,248. The next is may-minihttp with 237,925, range 229,668-240,700. Wisp is first on /page: 230,145, range 227,741-231,105. The next is may-minihttp with 198,464, range 191,616-202,414. Wisp has the lowest CPU per request on every path. On /plaintext and /json Wisp is second: 289,884 against may-minihttp 296,388, and 289,916 against 296,149. The ranges overlap (Wisp /plaintext 288,647-290,392, may-minihttp 277,200-297,577), so those two paths are ties. Peak memory is 5.7 MB, first of 20 (next hyper, 7.8 MB). Where Wisp does not win: the two ties above and deploy size (2.2 MB, 4th of 9, best Bun at 6 KB). The machine gate after the run failed on load1 3.91 (limit 0.5). The run does not meet the quiet-pipeline rule for a clean end.
Twelve Frameworks, Native, Linux
Not measured at 1.0.0-pre.3 yet.
Leptos, Dioxus and Actix Web: Instructions per Request
Release 1.0.0-pre.3, Wisp 2362ec0d, 2026-10-26. Instructions per request on the server thread (callgrind, one CPU, 100 warm-up and 1,000 measured requests), median of 3 reps (min-max), release builds. Source: bench/rust-fastest/results/ir.txt and ir-provenance.txt in the Wisp repo. Wisp is not measured at 1.0.0-pre.3: the spread of its 3 reps was above 1%.
| Route | Leptos | Dioxus | Actix Web |
|---|---|---|---|
/plaintext | 14,341 (14,341-14,344) | 13,750 (13,749-13,754) | 17,123 (17,118-17,129) |
/json | 14,906 (14,902-14,907) | 14,294 (14,293-14,295) | 17,829 (17,815-17,862) |
/fortunes (13 rows, escaped, no database) | 165,437 (165,412-165,443) | 470,141 (469,546-471,754) | 43,815 (43,811-43,818) |
Build Time and Dev Reload, Linux Bench Box
Source: bench/build/results/report.md and summary.json in the Wisp repo (node bench/build/measure.mjs run <contender> --runs 3), release 1.0.0-pre.3, Wisp 0d2d15a0 (the run used 1e85167d: the tag plus bench-only commits), 2026-10-27. The bench/tfb apps ran on the 4-core bench box (SMT off, kernel 6.8.0-146), all in one run. Each build ran under the bench lock after the machine drained, 3 runs per cell, the median. A run with steal over 10% would run again (none did: steal was 0% in every run). Cold is a clean build with dependencies already downloaded. Warm is the same command again. Dev reload is a file saved in the running dev server until GET serves the new text. Rank in brackets.
| Framework | Cold build | Warm, no change | Warm, logic edit | Warm, template edit | Dev, logic edit | Dev, template edit |
|---|---|---|---|---|---|---|
| Wisp | 43.9 s (#5) | 38 ms (#1) | 14.9 s (#4) | 15.0 s (#3) | 1 ms (#1) | 48 ms (#2) |
| SvelteKit | 3.7 s (#1) | 3.7 s (#4) | 3.7 s (#1) | 3.7 s (#1) | 22 ms (#3) | 14 ms (#1) |
| Nuxt | 5.1 s (#2) | 5.0 s (#5) | 5.0 s (#2) | 5.0 s (#2) | 895 ms (#6) | 143 ms (#3) |
| Next.js | 19.8 s (#3) | 17.8 s (#6) | 17.8 s (#6) | 17.9 s (#4) | 211 ms (#4) | 332 ms (#4) |
| Axum | 28.6 s (#4) | 57 ms (#2) | 13.5 s (#3) | - | - | - |
| Actix Web | 67.5 s (#6) | 100 ms (#3) | 16.6 s (#5) | - | - | - |
| Hono (Node) | - | - | - | - | 254 ms (#5) | - |
| Hono (Bun) | - | - | - | - | 8 ms (#2) | - |
- Where Wisp does not win: a cold build (43.9 s, against SvelteKit 3.7 s, Nuxt 5.1 s and Axum 28.6 s) and a release build after an edit (14.9 s for logic, 15.0 s for a template, against SvelteKit 3.7 s and Axum 13.5 s), because a release build compiles Rust with fat LTO. In the dev server, a template edit shows in 48 ms against SvelteKit's 14 ms.
-means the framework has no such step (no build, or no dev server in its docs). - Dev, logic edit: a string literal edit swaps in without a compile (1 ms). A number or
boolliteral edit also swaps in. Any other Rust edit recompiles.
Node, Bun and Deno, Linux VPS, Ranked Run
Not measured at 1.0.0-pre.3 yet.
workerd, Linux VPS, Ranked Run
Not measured at 1.0.0-pre.3 yet.
Edge and JavaScript Hosts: Instructions per Request
Release 1.0.0-pre.3, Wisp 2362ec0d, 2026-10-26. What each server's process does per request, counted by perf stat instead of timed: user instructions of the server's process over oha -n 40000 -c 64, divided by the requests. Same 6 vCPU VPS, servers on cores 0-1, oha on 2-3. Each of three runs gives the median of 3 reps, and the table gives the median of the runs. A cell with - is not valid (fewer than 3 runs, or a spread (max - min) / median of 1% or more). Wisp raw sockets and workerd have no valid cell and are left out. k = thousand, M = million instructions. Lower is better. Source: bench/edge/README.md ("Latest pass") in the Wisp repo.
| Host | Server | / | /json-big | /params | /list1000 | /form | /table |
|---|---|---|---|---|---|---|---|
| Node | Wisp node:http | 84.35k | 611.01k | 71.93k | 2.12M | 162.62k | - |
| Node | Hono | 72.98k | 507.42k | 78.02k | 5.98M | - | - |
| Deno | Wisp Deno.serve | 29.98k | 742.26k | 49.27k | 2.50M | - | - |
| Deno | Hono | 19.26k | 517.03k | 42.14k | 6.20M | - | - |
The Bun rows are left out: their warm-up was 3000 requests, too short for Bun's JIT (counts spread 14% to 19%). ir.mjs now warms Bun with 50000 requests and counts 200000 (spread under 0.5%). The cold start counts moved 2% to 15% between runs, so they are not published. The Cloudflare wasm of this bench app (bench/rank/out/wisp-cf/app.wasm) is 463,900 bytes, 177,024 with gzip -9.
What Is Checked
| Check | What it does |
|---|---|
| Instructions per request | A change that touches the request path runs A/B against the build before it, counting CPU instructions for the same requests. A feature a route does not use must add none. |
| Startup self-tests | Each fast path (the I/O driver, parsers, caches) is proven at server start, and falls back to the plain path if the proof fails. |
| Validity per cell | bench/quiet checks each cell on its own: its spread over the repeats must stay in the limit. A run is publishable when the gates pass and at least 90% of its cells are valid. A table shows a cell that is not valid as "noisy, not shown", and a cell that is missing in a repeat as "missing, not shown". A Wisp cell with a rival not shown gets no place: "valid cells only, N rival(s) noisy". A connection count with no calibration blocks the run. |
| Comparable frameworks | bench/ holds the same server-rendered page, JSON and plaintext routes written the way each framework's docs would write them, a load generator and a runner. |
Tokens
Writing cost is measured by a program in the repository: cargo run -p wisp-tokens --release. See Tokens.
Is This Page Useful?