Skip to content

Pack responses into the buffer pool the cache already has - #661

Merged
folbricht merged 1 commit into
masterfrom
pool-response-packing
Sep 27, 2026
Merged

folbricht merged 1 commit into
masterfrom
pool-response-packing

Conversation

@folbricht

@folbricht folbricht commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Four places pack a message into wire format themselves rather than letting the dns library do it, and each allocated a fresh buffer for every query: the DoH, DoQ and ODoH listeners packing an answer, and the net.Resolver adapter used for bootstrap lookups. Plain DNS and DoT get pooling from the dns library and were already unaffected.

The buffer pool the cache backends use moves to packbuf.go, since it is no longer only theirs, and gains packToPool to pack into one. Over a million DoH queries answering five A records, that is 1282 MB allocated down to 1008 MB and 409 GC cycles down to 328. Wall time per query is unchanged within the noise of the machine this was measured on; the saving is in allocation volume and collector work, which is what matters on a router.

DoQ also built its length-prefixed frame with a second allocation. The prefix now goes into another pooled buffer, which takes the response path there from two allocations and 576 bytes per query to none.

A buffer is only recycled when the write that consumed it succeeded. A failed write can leave the transport holding the bytes: an HTTP/2 handler whose connection goes away mid-write returns while the frame is still queued, and a reset QUIC stream leaves the data with the sender. Reusing the buffer then could put the next answer packed into it on the wire in place of the one still to be sent.

ODoH and the net.Resolver adapter are done with the bytes before they return, so they hand the buffer back unconditionally. ODoH encrypts the packed message into a buffer of its own, and the adapter copies into the caller's.

Four places pack a message into wire format themselves rather than
letting the dns library do it, and each allocated a fresh buffer for
every query: the DoH, DoQ and ODoH listeners packing an answer, and the
net.Resolver adapter used for bootstrap lookups. Plain DNS and DoT get
pooling from the dns library and were already unaffected.

The buffer pool the cache backends use moves to packbuf.go, since it is
no longer only theirs, and gains packToPool to pack into one. Over a
million DoH queries answering five A records, that is 1282 MB allocated
down to 1008 MB and 409 GC cycles down to 328.

DoQ also built its length-prefixed frame with a second allocation. The
prefix now goes into another pooled buffer, which takes the response
path there from two allocations per query to none.

A buffer is only recycled when the write that consumed it succeeded. A
failed write can leave the transport holding the bytes: an HTTP/2
handler whose connection goes away mid-write returns while the frame is
still queued, and a reset QUIC stream leaves the data with the sender.
Reusing the buffer then could put the next answer packed into it on the
wire in place of the one still to be sent.
@folbricht
folbricht force-pushed the pool-response-packing branch from c12abe7 to eefe7c0 Compare September 27, 2026 16:54
@folbricht
folbricht merged commit ddcd945 into master Sep 27, 2026
16 checks passed
@folbricht
folbricht deleted the pool-response-packing branch September 27, 2026 18:01
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