Skip to content

Write JSON results through a chunked pipe buffer - #10552

Merged
tobias-tengler merged 3 commits into
mainfrom
tte/json-result-chunked-pipe-writer
Oct 16, 2026
Merged

tobias-tengler merged 3 commits into
mainfrom
tte/json-result-chunked-pipe-writer

Conversation

@tobias-tengler

@tobias-tengler tobias-tengler commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

The response formatters write every JSON token straight into Kestrel's response PipeWriter, which takes a lock on each GetSpan and Advance, so a response pays thousands of lock operations.

Every formatter now writes through a ChunkedPipeBufferWriter: a 64 KB buffer that is copied to the pipe when it is full or the formatter flushes. The buffer is rented on the first write and returned as soon as it is copied, before the flush is awaited, so a stream holds nothing between events. A write of 4 KB or more goes to the pipe directly. The writer is cached per thread, so a response allocates nothing it did not allocate before. Flush points are unchanged: single results flush once at the end, streams per event.

Benchmarks

Results are written into a plain pipe and into a pipe that locks on every buffer call like Kestrel's response writer. The 10 KB and 47 KB results are the gateway benchmark response with one and ten users, as a single result, a batch of five and an event stream of five. The small results are product lists from 292 bytes to 5.6 KB, and the ones below 4 KB also run as a batch of five and an event stream of five. The scalar results are four pre-encoded UTF-8 strings of the given size, written the way Fusion writes source result strings.

Small result, 292 bytes

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 1.11 µs 1.00 432 B 1.00
Pipe PR 714 ns 0.64 432 B 1.00
Locking pipe main 2.05 µs 1.00 432 B 1.00
Locking pipe PR 744 ns 0.36 432 B 1.00

Small result, 1.1 KB

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 3.83 µs 1.00 1.24 KB 1.00
Pipe PR 2.22 µs 0.58 1.24 KB 1.00
Locking pipe main 7.03 µs 1.00 1.24 KB 1.00
Locking pipe PR 2.27 µs 0.32 1.24 KB 1.00

Small result, 2.7 KB

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 9.22 µs 1.00 2.88 KB 1.00
Pipe PR 5.34 µs 0.58 2.88 KB 1.00
Locking pipe main 16.9 µs 1.00 2.88 KB 1.00
Locking pipe PR 5.36 µs 0.32 2.88 KB 1.00

Small result, 5.6 KB

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 18.4 µs 1.00 5.62 KB 1.00
Pipe PR 10.6 µs 0.58 5.62 KB 1.00
Locking pipe main 33.7 µs 1.00 5.62 KB 1.00
Locking pipe PR 10.9 µs 0.32 5.62 KB 1.00

Four 2 KB scalars

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 495 ns 1.00 256 B 1.00
Pipe PR 558 ns 1.13 256 B 1.00
Locking pipe main 712 ns 1.00 256 B 1.00
Locking pipe PR 607 ns 0.85 256 B 1.00

Single 10 KB result

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 30.6 µs 1.00 6.05 KB 1.00
Pipe PR 18.9 µs 0.62 6.05 KB 1.00
Locking pipe main 51.3 µs 1.00 6.05 KB 1.00
Locking pipe PR 19.0 µs 0.37 6.05 KB 1.00

Single 47 KB result

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 138.8 µs 1.00 27.22 KB 1.00
Pipe PR 83.6 µs 0.60 27.22 KB 1.00
Locking pipe main 234.0 µs 1.00 27.22 KB 1.00
Locking pipe PR 83.4 µs 0.36 27.22 KB 1.00

Batch of five 292 byte results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 5.70 µs 1.00 3.57 KB 1.00
Pipe PR 3.25 µs 0.57 3.57 KB 1.00
Locking pipe main 10.4 µs 1.00 3.57 KB 1.00
Locking pipe PR 3.29 µs 0.32 3.57 KB 1.00

Event stream of five 292 byte results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 6.33 µs 1.00 3.88 KB 1.00
Pipe PR 3.92 µs 0.62 3.88 KB 1.00
Locking pipe main 11.1 µs 1.00 3.88 KB 1.00
Locking pipe PR 4.05 µs 0.37 3.88 KB 1.00

Batch of five 1.1 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 19.2 µs 1.00 7.67 KB 1.00
Pipe PR 10.6 µs 0.55 7.67 KB 1.00
Locking pipe main 35.3 µs 1.00 7.67 KB 1.00
Locking pipe PR 10.7 µs 0.30 7.67 KB 1.00

Event stream of five 1.1 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 19.8 µs 1.00 7.98 KB 1.00
Pipe PR 11.3 µs 0.57 7.98 KB 1.00
Locking pipe main 36.0 µs 1.00 7.98 KB 1.00
Locking pipe PR 11.5 µs 0.32 7.98 KB 1.00

Batch of five 2.7 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 46.6 µs 1.00 15.88 KB 1.00
Pipe PR 24.9 µs 0.53 15.88 KB 1.00
Locking pipe main 84.8 µs 1.00 15.88 KB 1.00
Locking pipe PR 25.0 µs 0.29 15.88 KB 1.00

Event stream of five 2.7 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 46.8 µs 1.00 16.18 KB 1.00
Pipe PR 25.9 µs 0.55 16.18 KB 1.00
Locking pipe main 85.5 µs 1.00 16.18 KB 1.00
Locking pipe PR 25.9 µs 0.30 16.18 KB 1.00

Batch of five 10 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 152.8 µs 1.00 31.74 KB 1.00
Pipe PR 94.0 µs 0.61 31.74 KB 1.00
Locking pipe main 263.2 µs 1.00 31.74 KB 1.00
Locking pipe PR 95.0 µs 0.36 31.74 KB 1.00

Event stream of five 10 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 155.2 µs 1.00 32.04 KB 1.00
Pipe PR 94.0 µs 0.61 32.04 KB 1.00
Locking pipe main 260.5 µs 1.00 32.04 KB 1.00
Locking pipe PR 94.3 µs 0.36 32.04 KB 1.00

Batch of five 47 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 692.4 µs 1.00 137.56 KB 1.00
Pipe PR 423.4 µs 0.61 137.56 KB 1.00
Locking pipe main 1,161.5 µs 1.00 137.56 KB 1.00
Locking pipe PR 424.2 µs 0.37 137.56 KB 1.00

Event stream of five 47 KB results

Method Version Mean Ratio Allocated Alloc Ratio
Pipe main 694.5 µs 1.00 137.86 KB 1.00
Pipe PR 422.7 µs 0.61 137.86 KB 1.00
Locking pipe main 1,170.9 µs 1.00 137.87 KB 1.00
Locking pipe PR 423.9 µs 0.36 137.86 KB 1.00

The response formatters write each response into a 64 KB buffer that is copied to
the response pipe when it is full or the formatter flushes, instead of writing every
token straight into Kestrel's pipe. The buffer is held only while a payload is being
written, a write of 4 KB or more goes to the pipe directly, and the writer is cached
per thread so a response allocates nothing it did not allocate before.
Copilot AI balanced review requested due to automatic review settings October 15, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Patch coverage

83.2% of changed lines covered (124/149)

File Covered Changed Patch %
…/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs 15 19 78.9% 🔴
…/src/Transport.Formatters/ChunkedPipeBufferWriter.cs 85 105 81.0% 🟡
…/src/Transport.Formatters/JsonLinesResultFormatter.cs 7 8 87.5% 🟡
…/src/Transport.Formatters/EventStreamResultFormatter.cs 8 8 100.0% 🟢
…/src/Transport.Formatters/MultiPartResultFormatter.cs 7 7 100.0% 🟢
…/AspNetCore/src/Transport.Formatters/ThrowHelper.cs 2 2 100.0% 🟢
Uncovered changed lines (JSON)
{
  "sha": "c12b5734a56974c0f53e0aa99c7695a39c5d7c6a",
  "files": [
    { "path": "src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs", "ranges": [[245, 248]] },
    { "path": "src/HotChocolate/AspNetCore/src/Transport.Formatters/ChunkedPipeBufferWriter.cs", "ranges": [[70, 73], [76, 77], [99, 99], [102, 106], [108, 110], [157, 159], [190, 191]] },
    { "path": "src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs", "ranges": [[50, 50]] }
  ]
}

Project coverage: 59.0% (311132/527557 lines)

A message whose formatting throws is discarded from the buffer, so the next
message of a batch or stream does not continue a truncated one. Advance is
validated against the buffer like the pipe does. The multipart batch reuses
its writer for streams instead of renting a second one.
@tobias-tengler
tobias-tengler merged commit 78fd3d8 into main Oct 16, 2026
158 checks passed
@tobias-tengler
tobias-tengler deleted the tte/json-result-chunked-pipe-writer branch October 16, 2026 06:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants