Skip to content

perf(http-result): send buffered responses with Content-Length - #108

Merged
Upd4ting merged 1 commit into
mainfrom
perf/http-result-content-length
Nov 10, 2026
Merged

Upd4ting merged 1 commit into
mainfrom
perf/http-result-content-length

Conversation

@Upd4ting

Copy link
Copy Markdown
Member

Why

Every AntelopeJS app answers through HTTPResult.sendResponse. It called writeHead before end(body), so Node never learned the body length: every buffered response went out Transfer-Encoding: chunked. Node then writes it as a writev of header, chunk size, body and terminator, which took 30% of the server's CPU on a small JSON route. With a Content-Length, Node writes header and body in one write. This settles the ANT-1915 lead with an interleaved A/B through the api module. ANT-1925.

What changed

  • sendResponse adds Content-Length: Buffer.byteLength(body) to a buffered response.
  • It adds none when the status carries no body (1xx, 204, 304, read as Node truncates it), or when the result's headers or headers already set on rawResponse set Content-Length, Transfer-Encoding or Trailer. Streams stay chunked.
  • docs/3.http-handling.md and the sendResponse TSDoc state the rule. The result-send verify journey expects the new header.

Scope

  • Buffered responses only. sendHeadResponse and stream responses are unchanged.
  • No exported name or type changes.

Tradeoffs

  • res.statusCode + setHeader + end(body) lets Node add the length itself, but measured 5% slower than today: setHeader validation cost more than the single write saved.
  • Passing Content-Length unconditionally to writeHead is faster still but sends it on 204 (RFC 9110 forbids that) and throws ERR_HTTP_TRAILER_INVALID when a Trailer header is set.

Blast Radius

Every non-HEAD, non-stream response of every AntelopeJS app changes framing from chunked to Content-Length. Body, status and other headers are unchanged. Clients handle both framings. Trailers added with rawResponse.addTrailers but not declared in a Trailer header are no longer sent, the same as Node's own res.end(body). A sendResponse argument must now have getHeaderNames, which any ServerResponse has; the only caller, @antelopejs/api, passes a real one.

Door: two-way. One-way-door: contract flags src/http-result.ts: response framing is part of the HTTP contract, and a revert restores chunked framing with no data to migrate.

Verification

  • Workload. @antelopejs/api 1.7.0 linked to this build (ajs module test on a bench controller), autocannon -c 50 -d 5 -w 2 after a 2 s warm-up, server CPU at 100%. 9 rounds, base and change interleaved, server restarted for each run. Evidence level: confirmed through end-to-end benchmark and CPU profile.

    route before, median rps [range] after, median rps [range] change
    text ("hello") 39,869 [35,963–40,957] 44,144 [41,699–46,179] +10.7%
    j10 (689 B JSON) 32,240 [31,222–33,392] 36,042 [34,203–37,322] +11.8%
    result (201 HTTPResult) 32,414 [28,955–32,770] 34,858 [32,981–35,952] +7.5%

    The change won all 9 paired rounds on each route, and no ranges overlap. A 140 KB JSON body measured −0.6% (neutral): JSON.stringify dominates there. Profile: writev 31.3% self time → a single writeUtf8String. Cumulative impact: about 2.4 µs of CPU saved per small response, so ~12 ms CPU/s at 5,000 req/s.

  • Tests. The new HTTPResult body framing suite sends through a real loopback server. On origin/main, "Frames a buffered body with its byte length" fails (content-length is null, response chunked); after, all 13 cases in http-result.test.js pass. The trailer and raw-response cases fail on a version that always adds the length (ERR_HTTP_TRAILER_INVALID, duplicate framing).

  • antelopejs-verify journey --plan: 18/18 journeys pass. pnpm run checks: 11 passed, 0 failed.

before: HTTP/1.1 200 OK | Content-Type: text/plain | Transfer-Encoding: chunked | 5\r\nhello\r\n0\r\n\r\n
after:  HTTP/1.1 200 OK | Content-Type: text/plain | Content-Length: 5         | hello

@Upd4ting
Upd4ting merged commit 122a1c2 into main Nov 10, 2026
8 checks passed
@Upd4ting
Upd4ting deleted the perf/http-result-content-length branch November 10, 2026 21:22
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