[go] net/http/internal/http2: don't pin frame-sized write buffers on idle conns

5 views
Skip to first unread message

Brad Fitzpatrick (Gerrit)

unread,
Sep 5, 2026, 6:38:09 PMSep 5
to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

Brad Fitzpatrick has uploaded the change for review

Commit message

net/http/internal/http2: don't pin frame-sized write buffers on idle conns

A Framer's wbuf grows to the size of the largest frame written
(typically a full DATA frame) and was retained for the life of the
connection, pinning several kB per idle connection.

The frame is fully written out by the end of endWrite, so release
large write buffers to a sync.Pool there and have startWrite adopt a
pooled buffer for the next frame. Buffers up to 1 kB stay attached to
their Framer, so connections writing only small control frames don't
pay for the pooling.

Measured with 1000 idle HTTP/2 connections on separate TCP flows
after POST exchanges with 4 kB responses (heap bytes per connection,
runtime.MemProfileRate=1, HPACK dynamic tables disabled, and with the
corresponding read buffer fix already applied):

HeapAlloc/conn: 15265 B -> 10377 B
HeapInuse/conn: 20357 B -> 15114 B

http2 benchmarks are unchanged within noise. The largest remaining
consumers are the serverConn struct (~1.8 kB), TLS cipher state
(~1.8 kB), and the tls.Conn struct (~0.9 kB).

Updates #80735
Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95

Change diff

diff --git a/src/net/http/internal/http2/frame.go b/src/net/http/internal/http2/frame.go
index 4632259..5567c2a 100644
--- a/src/net/http/internal/http2/frame.go
+++ b/src/net/http/internal/http2/frame.go
@@ -310,8 +310,16 @@

maxWriteSize uint32 // zero means unlimited; TODO: implement

- w io.Writer
+ w io.Writer
+ // wbuf is the frame currently being written, from startWrite's
+ // header through endWrite backfilling the length and writing it
+ // out. endWrite releases large buffers to writeBufPool so that
+ // idle connections don't pin a frame-sized buffer.
wbuf []byte
+ // wbufP, if non-nil, is the pooled container holding wbuf's array,
+ // to be returned to writeBufPool by endWrite. It is nil when wbuf
+ // is small (under maxIdleWriteBufCap) and not worth pooling.
+ wbufP *[]byte

// AllowIllegalWrites permits the Framer's Write methods to
// write frames that do not conform to the HTTP/2 spec. This
@@ -372,7 +380,27 @@
return fr.MaxHeaderValueCount
}

+// writeBufPool pools the frame-sized buffers that back Framer.wbuf.
+// endWrite returns the buffer to the pool once the frame has been
+// written out, so that idle connections don't each pin a buffer sized
+// by the last frame they wrote.
+var writeBufPool sync.Pool
+
+// maxIdleWriteBufCap is the largest wbuf capacity that a Framer keeps
+// between frame writes. It is large enough for control frames and
+// small HEADERS frames, so that only connections writing larger frames
+// pay for the buffer pooling.
+const maxIdleWriteBufCap = 1024
+
func (f *Framer) startWrite(ftype FrameType, flags Flags, streamID uint32) {
+ if f.wbuf == nil {
+ // endWrite released the previous buffer; start with a pooled
+ // one if available. Appends below grow it as needed.
+ if bp, _ := writeBufPool.Get().(*[]byte); bp != nil {
+ f.wbufP = bp
+ f.wbuf = (*bp)[:0]
+ }
+ }
// Write the FrameHeader.
f.wbuf = append(f.wbuf[:0],
0, // 3 bytes of length, filled in endWrite
@@ -405,9 +433,31 @@
if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
+ f.releaseWriteBuf()
return err
}

+// releaseWriteBuf returns a large wbuf, whose frame has been written
+// out, to writeBufPool, so that a connection idle between writes does
+// not pin a frame-sized buffer. Small buffers stay attached to the
+// Framer.
+func (f *Framer) releaseWriteBuf() {
+ if cap(f.wbuf) <= maxIdleWriteBufCap {
+ return
+ }
+ bp := f.wbufP
+ if bp == nil {
+ bp = new([]byte)
+ }
+ // If appends outgrew a pooled array, the container is recycled
+ // with the new larger array and the old one is dropped, so the
+ // pool converges toward large buffers.
+ *bp = f.wbuf[:0]
+ writeBufPool.Put(bp)
+ f.wbufP = nil
+ f.wbuf = nil
+}
+
func (f *Framer) logWrite() {
if f.debugFramer == nil {
f.debugFramerBuf = new(bytes.Buffer)
diff --git a/src/net/http/internal/http2/frame_test.go b/src/net/http/internal/http2/frame_test.go
index 009e0ad..2d3318b 100644
--- a/src/net/http/internal/http2/frame_test.go
+++ b/src/net/http/internal/http2/frame_test.go
@@ -1723,3 +1723,45 @@
t.Error("accessing previous frame's Data after failed ReadFrame did not panic")
}
}
+
+func TestFramerWriteBufReleased(t *testing.T) {
+ fr, buf := testFramer()
+ if err := fr.WriteData(1, false, make([]byte, 2*maxIdleWriteBufCap)); err != nil {
+ t.Fatal(err)
+ }
+ if fr.wbuf != nil || fr.wbufP != nil {
+ t.Errorf("after large write: wbuf = %v (cap %d), wbufP = %v; want both released",
+ fr.wbuf != nil, cap(fr.wbuf), fr.wbufP != nil)
+ }
+
+ // Subsequent writes still produce well-formed frames.
+ if err := fr.WritePing(false, [8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := fr.ReadFrame(); err != nil { // the DATA frame
+ t.Fatal(err)
+ }
+ f, err := fr.ReadFrame()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := f.(*PingFrame); !ok {
+ t.Errorf("second frame = %T, want *PingFrame", f)
+ }
+ _ = buf
+}
+
+func TestFramerWriteBufSmallKept(t *testing.T) {
+ fr, _ := testFramer()
+ fr.wbuf = make([]byte, 0, 64) // small buffer stays attached across writes
+ if err := fr.WritePing(false, [8]byte{}); err != nil {
+ t.Fatal(err)
+ }
+ if fr.wbuf == nil || cap(fr.wbuf) != 64 {
+ t.Errorf("after small write: cap(wbuf) = %d (nil=%v), want the seeded 64-byte buffer kept",
+ cap(fr.wbuf), fr.wbuf == nil)
+ }
+ if fr.wbufP != nil {
+ t.Error("after small write: wbufP != nil, want nil")
+ }
+}

Change information

Files:
  • M src/net/http/internal/http2/frame.go
  • M src/net/http/internal/http2/frame_test.go
Change size: M
Delta: 2 files changed, 93 insertions(+), 1 deletion(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
Gerrit-Change-Number: 828206
Gerrit-PatchSet: 1
Gerrit-Owner: Brad Fitzpatrick <brad...@golang.org>
unsatisfied_requirement
satisfied_requirement
open
diffy

Brad Fitzpatrick (Gerrit)

unread,
Sep 5, 2026, 6:45:36 PMSep 5
to Brad Fitzpatrick, goph...@pubsubhelper.golang.org, Nicholas Husin, Damien Neil, golang-co...@googlegroups.com
Attention needed from Damien Neil and Nicholas Husin

Brad Fitzpatrick voted Commit-Queue+1

Commit-Queue+1
Open in Gerrit

Related details

Attention is currently required from:
  • Damien Neil
  • Nicholas Husin
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
Gerrit-Change-Number: 828206
Gerrit-PatchSet: 1
Gerrit-Owner: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
Gerrit-Attention: Damien Neil <dn...@google.com>
Gerrit-Attention: Nicholas Husin <n...@golang.org>
Gerrit-Comment-Date: Sat, 05 Sep 2026 22:45:30 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy

Damien Neil (Gerrit)

unread,
Sep 8, 2026, 5:00:09 PMSep 8
to Brad Fitzpatrick, goph...@pubsubhelper.golang.org, golang...@luci-project-accounts.iam.gserviceaccount.com, Nicholas Husin, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick and Nicholas Husin

Damien Neil voted and added 1 comment

Votes added by Damien Neil

Code-Review+2

1 comment

File src/net/http/internal/http2/frame.go
Line 313, Patchset 1 (Latest): w io.Writer
Damien Neil . unresolved

We've got a []byte write buffer here, but this is always a *bufio.Writer (or a *bufferedWriter which contains a *bufio.Writer).

Too many buffers!

Anyway, this change seems like an improvement over what we have, but I'm pretty sure we could make this all much better.

Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
  • Nicholas Husin
Submit Requirements:
  • requirement satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
Gerrit-Change-Number: 828206
Gerrit-PatchSet: 1
Gerrit-Owner: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Attention: Nicholas Husin <n...@golang.org>
Gerrit-Comment-Date: Tue, 08 Sep 2026 21:00:05 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
satisfied_requirement
unsatisfied_requirement
open
diffy

Brad Fitzpatrick (Gerrit)

unread,
Sep 8, 2026, 7:16:00 PMSep 8
to Brad Fitzpatrick, goph...@pubsubhelper.golang.org, Damien Neil, golang...@luci-project-accounts.iam.gserviceaccount.com, Nicholas Husin, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick and Nicholas Husin

Brad Fitzpatrick added 1 comment

File src/net/http/internal/http2/frame.go
Damien Neil . resolved

We've got a []byte write buffer here, but this is always a *bufio.Writer (or a *bufferedWriter which contains a *bufio.Writer).

Too many buffers!

Anyway, this change seems like an improvement over what we have, but I'm pretty sure we could make this all much better.

Brad Fitzpatrick

Agreed. I wasn't sure how much appetite you had for churn here. :)

Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
  • Nicholas Husin
Submit Requirements:
  • requirement satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
Gerrit-Change-Number: 828206
Gerrit-PatchSet: 1
Gerrit-Owner: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
Gerrit-CC: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Attention: Nicholas Husin <n...@golang.org>
Gerrit-Comment-Date: Tue, 08 Sep 2026 23:15:55 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Damien Neil <dn...@google.com>
satisfied_requirement
unsatisfied_requirement
open
diffy

Nicholas Husin (Gerrit)

unread,
Sep 9, 2026, 9:50:18 AMSep 9
to Brad Fitzpatrick, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Damien Neil, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick

Nicholas Husin voted Code-Review+2

Code-Review+2
Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
Submit Requirements:
  • requirement satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
Gerrit-Change-Number: 828206
Gerrit-PatchSet: 1
Gerrit-Owner: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
Gerrit-CC: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Comment-Date: Wed, 09 Sep 2026 13:50:13 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
satisfied_requirement
unsatisfied_requirement
open
diffy

Nicholas Husin (Gerrit)

unread,
Sep 9, 2026, 9:50:27 AMSep 9
to Brad Fitzpatrick, goph...@pubsubhelper.golang.org, Nicholas Husin, Brad Fitzpatrick, Damien Neil, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com
Attention needed from Brad Fitzpatrick

Nicholas Husin voted Code-Review+1

Code-Review+1
Open in Gerrit

Related details

Attention is currently required from:
  • Brad Fitzpatrick
Submit Requirements:
    • requirement satisfiedCode-Review
    • requirement satisfiedNo-Unresolved-Comments
    • requirement satisfiedReview-Enforcement
    • requirement satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: go
    Gerrit-Branch: master
    Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
    Gerrit-Change-Number: 828206
    Gerrit-PatchSet: 1
    Gerrit-Owner: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Reviewer: Damien Neil <dn...@google.com>
    Gerrit-Reviewer: Nicholas Husin <hu...@google.com>
    Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
    Gerrit-CC: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
    Gerrit-Comment-Date: Wed, 09 Sep 2026 13:50:21 +0000
    Gerrit-HasComments: No
    Gerrit-Has-Labels: Yes
    satisfied_requirement
    open
    diffy

    Brad Fitzpatrick (Gerrit)

    unread,
    Sep 9, 2026, 11:29:34 AMSep 9
    to Brad Fitzpatrick, goph...@pubsubhelper.golang.org, golang-...@googlegroups.com, Nicholas Husin, Nicholas Husin, Brad Fitzpatrick, Damien Neil, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com

    Brad Fitzpatrick submitted the change

    Change information

    Commit message:
    net/http/internal/http2: don't pin frame-sized write buffers on idle conns

    A Framer's wbuf grows to the size of the largest frame written
    (typically a full DATA frame) and was retained for the life of the
    connection, pinning several kB per idle connection.

    The frame is fully written out by the end of endWrite, so release
    large write buffers to a sync.Pool there and have startWrite adopt a
    pooled buffer for the next frame. Buffers up to 1 kB stay attached to
    their Framer, so connections writing only small control frames don't
    pay for the pooling.

    Measured with 1000 idle HTTP/2 connections on separate TCP flows
    after POST exchanges with 4 kB responses (heap bytes per connection,
    runtime.MemProfileRate=1, HPACK dynamic tables disabled, and with the
    corresponding read buffer fix already applied):

    HeapAlloc/conn: 15265 B -> 10377 B
    HeapInuse/conn: 20357 B -> 15114 B

    http2 benchmarks are unchanged within noise. The largest remaining
    consumers are the serverConn struct (~1.8 kB), TLS cipher state
    (~1.8 kB), and the tls.Conn struct (~0.9 kB).

    Updates #80735
    Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
    Files:
    • M src/net/http/internal/http2/frame.go
    • M src/net/http/internal/http2/frame_test.go
    Change size: M
    Delta: 2 files changed, 93 insertions(+), 1 deletion(-)
    Branch: refs/heads/master
    Submit Requirements:
    Open in Gerrit
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: merged
    Gerrit-Project: go
    Gerrit-Branch: master
    Gerrit-Change-Id: I7c1e08e2f6df944d34a2ef7a5f5091b1d3648b95
    Gerrit-Change-Number: 828206
    Gerrit-PatchSet: 2
    open
    diffy
    satisfied_requirement
    Reply all
    Reply to author
    Forward
    0 new messages