Skip to content

benchmark: add scenarios for the request shapes the suite never covered - #368

Closed
nigrosimone wants to merge 1 commit into
dimdenGD:mainfrom
nigrosimone:benchmark/representative-scenarios
Closed

benchmark: add scenarios for the request shapes the suite never covered#368
nigrosimone wants to merge 1 commit into
dimdenGD:mainfrom
nigrosimone:benchmark/representative-scenarios

Conversation

@nigrosimone

@nigrosimone nigrosimone commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

The three most common things an Express application does were not measured at all. Meanwhile five of the twelve rows sit on workloads dominated by node:zlib, node:sha256, JSON.parse or loopback bandwidth, where the ratio is capped by arithmetic, roughly 1.01x for the streaming rows and 1.02x for the 512 KiB JSON one, no matter what either framework does. The table currently reads as "these frameworks are mostly equivalent", when what it mostly measures is work neither of them performs.

This adds four rows for shapes that were missing, chosen for how common they are rather than for how they score.

routing/api-endpoint

GET /api/users/:userId/posts?fields=id,title,author&limit=10 through a mounted router, answering with res.json(). Param extraction, query parsing and serialisation, the shape where the framework's own work is a real share of the request rather than a rounding error.

middlewares/realistic-stack

helmet + cors + cookie-parser + express.json() + morgan, which is what a service actually mounts, as opposed to middlewares-100's hundred no-ops. morgan writes to a sink so the row measures the formatting every request pays rather than the runner's terminal, and combined is used because that is the production default, it includes the remote address, which each framework resolves its own way.

Worth saying plainly: this row is not favourable, and that is the point. Locally it comes out at 1.12x, because helmet, cors, cookie-parser and morgan are the same code on both sides. A realistic middleware stack is largely shared work, the same way zlib is. It is the row most relevant to somebody deciding whether to adopt, and it should say what it says.

middlewares/body-json-4kb

A body-parser row at a size an API actually receives. The 512 KiB one is kept as the stress case, that one measures JSON.parse, this one measures getting the bytes to it.

connections/high-concurrency

1000 connections against a trivial handler. Every other scenario runs at 50–200, where connection handling is free, so nothing in the suite exercised the part where uWS differs from node:http structurally rather than in the request path. Two wrk threads on a 4 vCPU runner leaves the server two; at this connection count some of what is measured is the generator, and the comment in the scenario says so.


Benchmark Comparison

Test Express req/sec uExpress req/sec Express throughput uExpress throughput uExpress speedup
connections/high-concurrency 14.04k 40.00k 2.22 MB/sec 6.37 MB/sec 2.87x
engines/art 5.46k 7.21k 2.39 MB/sec 3.17 MB/sec 1.33x
middlewares/body-json-4kb 9.57k 14.69k 1.52 MB/sec 2.34 MB/sec 1.54x
middlewares/body-json-512kb 897.18 953.04 147.19 KB/sec 157.29 KB/sec 1.07x
middlewares/body-urlencoded 10.07k 17.73k 1.56 MB/sec 2.77 MB/sec 1.78x
middlewares/compression-file 6.64k 6.92k 3.03 MB/sec 3.16 MB/sec 1.04x
middlewares/express-static 2.83k 5.10k 691.69 MB/sec 1.22 GB/sec 1.81x
middlewares/realistic-stack 9.23k 11.27k 7.88 MB/sec 9.64 MB/sec 1.22x
routing/api-endpoint 10.92k 26.19k 6.66 MB/sec 16.01 MB/sec 2.40x
routing/hello-world 15.14k 58.29k 2.51 MB/sec 9.73 MB/sec 3.88x
routing/middlewares-100 17.20k 11.95k 2.69 MB/sec 1.88 MB/sec 0.70x
routing/nested-routers 14.31k 47.95k 2.29 MB/sec 7.73 MB/sec 3.38x
routing/routes-1000 4.77k 36.39k 764.59 KB/sec 5.73 MB/sec 7.67x
streaming/readable-hash-4mb 223.53 249.94 49.77 KB/sec 55.90 KB/sec 1.12x
streaming/writable-no-content-length 539.81 560.64 2.64 GB/sec 2.74 GB/sec 1.04x
streaming/writable-with-content-length 537.62 573.95 2.63 GB/sec 2.81 GB/sec 1.07x

grep -r "res.json\|req.params\|req.query" over benchmark/scenarios returned
nothing. The three most common things an Express application does were not
measured at all, while five of the twelve rows sat on workloads dominated by
zlib, sha256, JSON.parse or loopback bandwidth, where the ratio is capped by
arithmetic no matter what either framework does.

routing/api-endpoint: GET /api/users/:userId/posts?fields=...&limit=10 through a
mounted router, answering with res.json(). Param extraction, query parsing and
serialisation - the shape where the framework's own work is a real share of the
request.

middlewares/realistic-stack: helmet + cors + cookie-parser + json + morgan,
which is what a service actually mounts, as opposed to middlewares-100's 100
no-ops. morgan writes to a sink so the row measures the formatting every request
pays rather than the runner's terminal, and 'combined' is used because that is
the production default.

middlewares/body-json-4kb: a body-parser row at a size an API actually receives.
The 512 KiB one is kept as the stress case - it measures JSON.parse, this one
measures getting the bytes to it.

connections/high-concurrency: 1000 connections against a trivial handler. Every
other scenario runs at 50-200, where connection handling is free, so nothing
exercised the part where uWS differs from node:http structurally.

All four verified to return identical status and body on both frameworks.
Local preview, 5 paired runs on a machine whose ratios run about a third of
CI's: api-endpoint 1.88x, body-json-4kb 1.19x, realistic-stack 1.12x.
@dimdenGD dimdenGD closed this Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants