Skip to content

Upstream Sync Report 2026-09-10: HTTP/2 Framer idle buffer pooling (memory optimization) #536

Description

@imroc

Upstream Changes

Go stdlib net/http landed a series of memory optimizations (tracking golang/go#80735) that reduce memory pinned by idle connections. Three of them live in src/net/http/internal/http2/frame.go and apply to req's inlined copy (internal/http2/frame.go, which tracks golang.org/x/net/http2):

Commit Subject Assessment
94819a5 internal/http2: turn Framer.getReadBuf func field into a method Pure refactor, no behavior change; preparation for the two pooling changes below
3127f7b internal/http2: don't pin frame-sized read buffers on idle conns Applies to req — read buffer released to a sync.Pool while blocked waiting for a frame header
64c0b14 internal/http2: don't pin frame-sized write buffers on idle conns Applies to req — frame-sized write buffer released to a sync.Pool in endWrite

Two more commits landed on the server side only and do not affect req (req does not inline net/http server code, and internal/http2/ has no server.go):

  • a14b543 net/http: populate Request.TLS from HTTP/2 connection state (only internal/http2/server.go)
  • e2a74e8 net/http: don't pin bufio buffers on idle HTTP/1 keep-alive connections (only net/http/server.go)

As of today golang.org/x/net/http2 has not picked these up yet (latest commit still 55577aa, 2026-08-28), so the stdlib commits are the reference to port from — same situation as the internal-http2 commits tracked in #527.

Impact on req

req is a client library; each ClientConn holds one Framer, and the readLoop calls cc.fr.ReadFrame() (internal/http2/transport.go:2457):

  • Read side: after receiving a large frame, Framer.readBuf stays at frame size (16 kB with the default max frame size) for the whole connection lifetime, including while the readLoop is blocked waiting for the next 9-byte frame header — potentially minutes or hours on pooled idle connections. Upstream measured 32715 B → 16363 B heap per idle connection with the fix.
  • Write side: Framer.wbuf grows to the largest frame written (typically a full DATA frame) and is never released. Upstream measured an additional ~4.9 kB saved per connection.

For workloads holding many idle HTTP/2 connections (the common scraping / API-client scenario req targets), this is a meaningful per-connection memory reduction. Buffers up to 1 kB stay attached to the Framer, so connections exchanging only small control frames pay nothing for the pooling.

Suggested sync

Port the three frame.go commits as a series into internal/http2/frame.go (merge upstream http2: <date>(<hash>) style). Port notes:

  • req's copy has a single ReadFrame (no ReadFrameForHeader split), so releaseReadBuf() goes at the top of ReadFrame(), before readFrameHeader blocks.
  • The req-specific customizations in ReadFrame (currentRequest, dump integration) and the cc *ClientConn field are orthogonal to buffer pooling and must be preserved as-is.
  • Adds bufio/sync imports, readBufP/wbufP fields, readBufPool/writeBufPool, and the 1 kB maxIdleReadBufCap/maxIdleWriteBufCap constants.
  • No public API change; no behavior change beyond buffer lifetime.

Not security-related, no workaround needed — pure memory optimization, low priority.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    http2Involves HTTP/2 or internal/http2 modified codeperformancePerformance-related issuepriority:lowMinor issue or feature request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions