Skip to content

http: cut per-request work on the server hot path - #30

Draft
anonrig wants to merge 7 commits into
mainfrom
cursor/http-perf-a3fb
Draft

anonrig wants to merge 7 commits into
mainfrom
cursor/http-perf-a3fb

Conversation

@anonrig

@anonrig anonrig commented Sep 19, 2026

Copy link
Copy Markdown
Owner

What

Every HTTP/1.1 request was paying for work most applications never need:

  • requireHostHeader (on by default) and the Expect check both read req.headers, which builds the whole headers object and toLowerCase()s every name.
  • writeHead() rebuilt the status line, Date: line, and Keep-Alive pair on every response.
  • Each request allocated two bound functions (updateOutgoingData, resOnFinish).
  • The parser created a fresh V8 string for every header name, including tokens like Host that appear on every request.

This change:

  1. Scans rawHeaders with a non-allocating ASCII compare for Host / Expect / Content-Length / Transfer-Encoding.
  2. Caches the default status line, the complete Date: …\r\n line, and the Connection + Keep-Alive pair.
  3. Reuses one pending-data callback and one 'finish' listener per connection.
  4. Interns header names in the C++ parser (kInternalized) so repeated tokens share one V8 string.

Wire format is unchanged. req.headers is still lazy and still correct when the application reads it, including unusual HOST / EXPECT casing (test/parallel/test-http-server-raw-header-lookup.js).

Benchmarks

Release build (./configure --without-intl --without-npm), same machine, server pinned with taskset -c 0, client is wrk -t2 -c50. Baseline is this tree at dd5dfb5250 (current nodejs/node main). Opt is this PR.

Official Node benches (one 5s run each):

bench baseline this PR Δ
http/simple.js bytes,len=4,c=50 111,290 req/s 117,556 +5.6%
http/incoming_headers.js headers=20 109,783 124,238 +13.2%

wrk hello-world, 8 extra request headers, 5 interleaved rounds of 8s (distributions do not overlap):

handler baseline median this PR median Δ
res.end('hello') 101,799 115,613 +13.6%
same, but read req.headers.host 94,629 103,678 +9.6%

The extra-header case is the realistic one: browsers and proxies send Host plus several other fields, and requireHostHeader used to materialize all of them.

Tests

python3 tools/test.py --mode=release --shell <opt-node>: all test/parallel/test-http-*.js that I ran passed (408 files across three batches).

Related

Overlaps with nodejs#65802 and nodejs#65332. Differences from nodejs#65802: the raw-header scan does not allocate toLowerCase() copies, Expect is resolved without materializing req.headers, HTTP/1.0 TE uses the same path, and header names are interned in the parser.

AI

A closed-source coding agent helped write and measure this. The HTTP hot path (parserOnIncoming, _storeHeader, CreateHeaders) was read and checked against the existing tests before opening the PR. Benchmark numbers above are from the two release binaries on this machine, not from a model.

Refs: nodejs#65802
Refs: nodejs#65332

Open in Web Open in Cursor 

@cursor
cursor Bot force-pushed the cursor/http-perf-a3fb branch 2 times, most recently from fbe7035 to 1fda05a Compare September 19, 2026 03:01
ayush23chaudhary and others added 6 commits September 19, 2026 03:52
Signed-off-by: Ayush Chaudhary <ayush23chaudhary@gmail.com>
PR-URL: nodejs#64251
Fixes: nodejs#64230
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Add `Buffer.stringLength(input[, encoding])`, the counterpart of
`Buffer.byteLength()`: it returns the number of UTF-16 code units that
`buf.toString(encoding)` would produce, without decoding.

For UTF-8 the count is computed with simdutf. Invalid input is counted
with the same maximal-subpart replacement that the decoder applies, so
the result always matches `toString().length`. The other encodings are
computed from `byteLength` alone.

This lets code that accumulates streamed input check the result against
`buffer.constants.MAX_STRING_LENGTH` and size its memory budget before
decoding.

Refs: nodejs#66062
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#66064
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
OpenSSL no longer applies DNS name constraints to the subject CN by
default, but tls.checkServerIdentity() still uses it without a DNS SAN.
Enable subject checking during chain verification only when no DNS SAN
exists, preserving the existing hostname verification policy.

Assisted-by: Codex
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: nodejs#65957
Refs: https://openssl-library.org/post/2026-09-09-openssl-4.1-alpha/
Reviewed-By: Tim Perry <pimterry@gmail.com>
`inspector.Session#connectToMainThread()` in a Worker aborted the
process on `CHECK_NOT_NULL(parent_handle_)` in
`Agent::ConnectToMainThread()` when the parent Environment was created
with `kNoCreateInspector`, as embedders that run their own inspector
(or none) do.

Throw the new `ERR_INSPECTOR_NOT_AVAILABLE` in that case, next to the
existing throw for a Worker whose own inspector is not initialized.

Refs: nodejs#35025
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: nodejs#65976
Reviewed-By: Kohei Ueno <kohei.ueno119@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Remove allocations and repeated work performed for every request:

- reuse a per-connection updateOutgoingData closure and a shared
  'finish' listener instead of binding two functions per request,
  reaching resOnFinish through the connection state stored on the
  response
- cache the ServerResponse options object per server (custom response
  classes keep receiving a fresh object)
- check for Host, Expect, Content-Length and Transfer-Encoding by
  scanning rawHeaders instead of materializing req.headers, which was
  built (with per-name toLowerCase calls) for every request even when
  the application never reads it
- cache the rendered status line per status code when the reason
  phrase is the default, skipping its character validation
- cache the complete 'Date: ...' header line in the utcDate cache and
  the keep-alive header pair for the current server settings
- compute the lenient-validation option chain once per message

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#65802
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
After nodejs#65802, Host/Expect/body checks still allocate
toLowerCase() copies and Expect/HTTP/1.0 TE still
read req.headers. Compare names without allocating,
read those values from rawHeaders, intern parser
header names, and skip Title-Case toLowerCase on
common outgoing fields.

Refs: nodejs#65802
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Assisted-by: a closed-source coding agent
@cursor
cursor Bot force-pushed the cursor/http-perf-a3fb branch from 16a6107 to 9b786ff Compare September 19, 2026 12:03
Interning every parsed header name lets a client fill the V8
interned string table with unique tokens. Drop that and keep
names as ordinary strings.

Refs: nodejs#66120
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Assisted-by: a closed-source coding agent
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.

5 participants