Skip to content

perf: speed up middleware routes and whole-body uploads - #21

Merged
assetcorp merged 1 commit into
mainfrom
perf/secure-headers-and-uploads
Oct 21, 2026
Merged

assetcorp merged 1 commit into
mainfrom
perf/secure-headers-and-uploads

Conversation

@assetcorp

Copy link
Copy Markdown
Owner

Summary

  • Routes behind middleware now take the same direct path as routes without it, as long as they have no body reader.
  • A plain 200 that carries one block of default headers from middleware, such as secureHeaders(), goes out in one head write.
  • bytes() collects the body inside the body reader instead of through an async generator. The exchange calls the reader directly instead of through listeners.
  • The framing scan allocates less. isDecimalDigits checks character codes in a loop instead of matching a regular expression.
  • On the secure-headers routes, uhttp's extra time per request over uWebSockets.js falls from 88 ns to 60 ns on plain text, and from 164 ns to 139 ns on JSON.
  • A 16 KiB upload costs about 0.7 µs over uWebSockets.js, down from about 2.3 µs. Short-lived memory per upload falls from 6.7 KB to 3.4 KB.
  • A client that disconnects mid-body, or a request that passes its time limit, now rejects a pending bytes() call with the matching error.
  • Every security check stays in place.

Testing

  • Lint, typecheck, build, and all 964 tests pass.
  • New end-to-end tests:
    • A 512 KiB body that arrives in pieces while the handler waits on bytes().
    • A client that disconnects mid-upload gets REQUEST_ABORTED, and the next client is served.
    • A chunked body over maxBodyBytes gets a 413, the connection closes, and the handler never runs.
  • Two existing end-to-end tests in request-context.test.ts now run with no middleware, with synchronous middleware, and with asynchronous middleware.
  • The new tests were checked by breaking the new code on purpose, and they failed.
  • pnpm nx run uhttp:speed ran three times on a quiet machine. Results: 3.6% faster, 0.9% slower, and 1.6% slower than uWebSockets.js, all within the 5% limit.

Routes behind middleware without a body reader now take the lazy fast path,
and a plain 200 with one folded block of default headers goes out with one
head write. bytes() collects the body inside the reader instead of through
an async generator, the exchange calls the body reader directly instead of
through listeners, and the framing scan allocates less.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 88f826d6-0d65-4dd5-a993-94d14081207f
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Speed check

h2load sent GET /hello to uWebSockets.js, to uhttp before this PR, and to uhttp after this PR, in 10 rounds of 3 s each over 100 connections. Each figure is the average of two identical copies of a server, and machine noise is the largest gap between two copies in a round.

Does this PR make uhttp slower?

The job never fails on this table.

Round Before this PR After this PR Difference Machine noise
1 140,570 req/s 140,978 req/s 0.3% faster 2.8%
2 141,637 req/s 142,349 req/s 0.5% faster 1.9%
3 143,148 req/s 139,526 req/s 2.5% slower 3.6%
4 144,042 req/s 142,513 req/s 1.1% slower 2.0%
5 143,342 req/s 140,181 req/s 2.2% slower 0.8%
6 140,389 req/s 143,489 req/s 2.2% faster 4.8%
7 143,230 req/s 142,028 req/s 0.8% slower 4.9%
8 142,055 req/s 145,155 req/s 2.2% faster 7.0%
9 148,317 req/s 145,779 req/s 1.7% slower 5.2%
10 145,907 req/s 146,834 req/s 0.6% faster 2.3%
Median 143,189 req/s 142,431 req/s 0.5% slower 3.2%

uhttp is 0.5% slower after this PR than before it, which is less than the 3.2% that the job treats as machine noise, so this PR leaves the speed of uhttp unchanged.

How much slower is uhttp than uWebSockets.js?

The job fails when uhttp after this PR is more than 5.0% slower in the median round.

Round uWebSockets.js uhttp after this PR Difference Machine noise
1 144,898 req/s 140,978 req/s 2.7% slower 2.8%
2 144,775 req/s 142,349 req/s 1.7% slower 1.9%
3 147,110 req/s 139,526 req/s 5.2% slower 3.6%
4 142,385 req/s 142,513 req/s 0.1% faster 2.0%
5 146,861 req/s 140,181 req/s 4.5% slower 0.8%
6 144,222 req/s 143,489 req/s 0.5% slower 4.8%
7 145,630 req/s 142,028 req/s 2.5% slower 4.9%
8 151,523 req/s 145,155 req/s 4.2% slower 7.0%
9 146,442 req/s 145,779 req/s 0.5% slower 5.2%
10 149,013 req/s 146,834 req/s 1.5% slower 2.3%
Median 146,036 req/s 142,431 req/s 2.1% slower 3.2%

After this PR, uhttp is 2.1% slower than uWebSockets.js, and before this PR, it was 2.3% slower. The job passes, because the limit is 5.0% slower.

@assetcorp
assetcorp merged commit 2c38760 into main Oct 21, 2026
11 of 12 checks passed
@assetcorp
assetcorp deleted the perf/secure-headers-and-uploads branch October 21, 2026 19:27
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.

1 participant