Wisp is in pre-release, self-hosted since GitHub suspended us. Install it, or see the changelog.

Wisp

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:

CellWispActix Web
plaintext, 256 connections4,202,276 (4,074,733-4,286,248)1,773,586 (1,761,859-1,789,080)
plaintext, 1,024 connections4,129,701 (4,069,306-4,164,053)1,753,616 (1,736,349-1,773,433)
plaintext, 4,096 connections3,109,809 (3,003,151-3,121,421)1,674,872 (1,653,457-1,687,683)
plaintext, 16,384 connections1,847,760 (1,831,501-1,869,285)1,665,969 (1,644,969-1,680,444)
JSON, 16 connections335,701 (329,767-343,575)245,569 (238,648-249,673)
JSON, 32 connections377,967 (373,888-385,399)260,386 (256,579-264,747)
JSON, 64 connections418,494 (410,002-421,214)260,653 (257,125-265,430)
JSON, 128 connections446,152 (445,765-472,030)258,370 (254,423-261,640)
JSON, 256 connections450,239 (444,429-462,635)253,544 (248,001-257,058)
JSON, 512 connections460,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:

Contender2561024409616384
Wisp 4,202,2764,129,7013,109,8091,847,760
Axum 1,205,9271,197,2291,190,8181,102,918
Actix Web 1,773,5861,753,6161,674,8721,665,969
Express 120,912121,999120,171115,539
Fastify 176,034175,354170,289169,560
Hono on Node.js 112,019111,597112,070108,710
Hono on Bun 227,026229,660227,063229,415
SvelteKit 33,04229,64228,31429,188
Next.js 10,15510,83710,90210,593
Nuxt 92,97680,00663,90561,056

Rows whose min-max ranges overlap are ties. .

JSON, requests per second (median of the valid runs), by connections:

Contender163264128256512
Wisp 335,701377,967418,494446,152450,239460,034
Axum 220,485227,601228,588227,753226,414224,362
Actix Web 245,569260,386260,653258,370253,544250,995
Express 78,16880,68780,28479,32878,98178,990
Fastify 96,30699,94999,84898,41199,82398,052
Hono on Node.js 59,38759,99359,58159,28158,57958,510
Hono on Bun 169,558181,642194,523188,479213,293204,491
SvelteKit 27,72428,56328,81728,32728,72828,945
Next.js 5,0425,0625,0135,0204,9504,951
Nuxt 73,29681,65085,77579,62280,69079,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_driver answers 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/pageCPU on /page (us)
may-minihttp296,388296,149237,925198,46410.1
Wisp289,884289,916269,747230,1458.7
ntex271,107267,648229,074108,65018.3
hyper245,328243,830209,516179,41311.2
uWebSockets.js241,553220,582111,65443,87745.6
xitca-web233,004229,059197,782182,09511.0
Actix Web226,029220,625191,296177,47111.5
Vert.x221,567221,033125,027104,10619.0
Elysia211,822202,521103,60445,33644.1
fasthttp207,983185,31342,0768,761225.8
Fiber199,147179,81940,9368,560231.7
Bun192,437202,387118,19950,24539.9
Axum189,834188,386162,750141,28814.3
ASP.NET Core122,149121,51349,48830,26865.7
Gin104,25497,77333,0807,826251.9
Go net/http104,22697,06732,9157,812252.2
Fastify59,35156,34440,44025,89477.2
Express33,03331,38526,19218,654107.2
SvelteKit18,31616,5464,6033,258614.7
Next.js3,5163,8468565673511.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%.

RouteLeptosDioxusActix Web
/plaintext14,341 (14,341-14,344)13,750 (13,749-13,754)17,123 (17,118-17,129)
/json14,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.

FrameworkCold buildWarm, no changeWarm, logic editWarm, template editDev, logic editDev, template edit
Wisp43.9 s (#5)38 ms (#1)14.9 s (#4)15.0 s (#3)1 ms (#1)48 ms (#2)
SvelteKit3.7 s (#1)3.7 s (#4)3.7 s (#1)3.7 s (#1)22 ms (#3)14 ms (#1)
Nuxt5.1 s (#2)5.0 s (#5)5.0 s (#2)5.0 s (#2)895 ms (#6)143 ms (#3)
Next.js19.8 s (#3)17.8 s (#6)17.8 s (#6)17.9 s (#4)211 ms (#4)332 ms (#4)
Axum28.6 s (#4)57 ms (#2)13.5 s (#3)---
Actix Web67.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 bool literal 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.

HostServer//json-big/params/list1000/form/table
NodeWisp node:http84.35k611.01k71.93k2.12M162.62k-
NodeHono72.98k507.42k78.02k5.98M--
DenoWisp Deno.serve29.98k742.26k49.27k2.50M--
DenoHono19.26k517.03k42.14k6.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

CheckWhat it does
Instructions per requestA 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-testsEach 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 cellbench/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 frameworksbench/ 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?

View This Page's Source

Type to search every docs page and heading.