Skip to content

⚡ perf(adaptor): cut allocations on the net/http bridge - #4559

Merged
ReneWerner87 merged 3 commits into
mainfrom
claude/adaptor-middleware-perf-s6o17h
Jul 27, 2026
Merged

⚡ perf(adaptor): cut allocations on the net/http bridge#4559
ReneWerner87 merged 3 commits into
mainfrom
claude/adaptor-middleware-perf-s6o17h

Conversation

@gaby

@gaby gaby commented Jul 26, 2026

Copy link
Copy Markdown
Member

Description

Cuts allocations and per-request work in middleware/adaptor, mostly on the net/http → Fiber direction (FiberApp, FiberHandler, FiberHandlerFunc).

The dominant cost was invisible: io.Copy(req.BodyWriter(), limitedReader) allocated a fresh 32 KiB buffer for every single request, including requests with no body at all. io.Copy only skips its internal buffer when the source implements io.WriterTo or the destination implements io.ReaderFrom*io.LimitedReader does neither, and neither does fasthttp's requestBodyWriter. So every request through FiberApp paid a 32 KiB allocation regardless of body size.

Four changes, all internal to the package:

  1. Request body — copy through io.CopyBuffer with a buffer from the pool the package already keeps, and an io.LimitedReader that lives in the pooled ctx. http.NoBody skips the copy machinery entirely.
  2. Response headers — the copy-back was dst.Add(string(k), string(v)) per header, costing a key string, a value string and a one-element slice each. Keys and values are now packed into one string that the map entries re-slice, and the single-valued entries are carved out of one shared []string, so the copy-back is two allocations regardless of header count. Keys still go through textproto.CanonicalMIMEHeaderKey, so results are identical to Header.Add.
  3. Remote address — the "ip:port" fast path co-locates the IP storage with the net.TCPAddr in one allocation instead of two (address + IP slice). Deliberately not pooled: RemoteAddr()/RemoteIP() hand this value to user code that may keep it past the handler.
  4. Per-request odds and endsApp.Config() returns a 624-byte struct by value; the body limit is now read once at construction rather than copied per request. HTTPMiddleware calls the fasthttp adaptor directly instead of rebuilding HTTPHandler's wrapper on every request.

No exported API changes, and no behavior changes.

Changes introduced

  • Benchmarks: 6 runs per side, -benchtime 2s, compared with benchstat against 84ecfab. Every changed row below is p=0.002.

    Benchmark ns/op B/op allocs/op
    FiberHandlerFunc/No_Content 4454 → 797 (−82%) 32987 → 136 (−99.6%) 7 → 1
    FiberHandlerFunc_Parallel/No_Content 2688 → 312 (−88%) 32987 → 137 (−99.6%) 7 → 1
    FiberApp 14442 → 2663 (−82%) 33.3Ki → 1.20Ki (−96%) 25 → 12
    FiberApp_Parallel 13579 → 1246 (−91%) 33.3Ki → 1.20Ki (−96%) 25 → 12
    FiberApp_Body 16709 → 4706 (−72%) 38.4Ki → 6.23Ki (−84%) 30 → 23
    HTTPMiddleware 7541 → 6041 (−20%) 2240 → 2225 25 → 24
    HTTPHandlerWithContext 3453 → 3236 (−6%) unchanged 18
    HTTPHandler unchanged unchanged 18

    Benchmark_HTTPMiddleware, Benchmark_FiberApp, Benchmark_FiberApp_Body and Benchmark_FiberApp_Parallel are new — those paths had no benchmark coverage at all.

  • Changelog/What's New: middleware/adaptor no longer allocates a 32 KiB buffer per request on the net/http → Fiber path; requests through FiberApp drop from 25 to 12 allocations and from ~33 KiB to ~1.2 KiB per request.

  • API Longevity: no exported identifiers added, removed or changed. The whole diff is package-internal.

  • Documentation Update: not needed — no exported API or documented behavior changed.

  • Migration Guide: not needed — no breaking change.

  • API Alignment with Express: N/A, internal performance work.

  • Examples: N/A.

Type of change

  • Performance improvement (non-breaking change which improves efficiency)

Checklist

  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Added or updated unit tests to validate the effectiveness of the changes or new features.
  • Ensured that new and existing unit tests pass locally with the changes.
  • Aimed for optimal performance with minimal allocations in the new code.
  • Provided benchmarks for the new code to analyze and improve upon.
  • No new dependencies.
  • Documentation in /docs/ — untouched, nothing user-facing changed.

New tests cover the header packing (multi-value headers, non-canonical keys, a destination that already holds values, and that entries sharing the []string backing array stay independent when appended to), that response header strings do not alias the pooled staging buffer, that the http.NoBody skip still reports Content-Length: 0, and that a remote address retained by a handler still reports its own client after the ctx pool recycles the entry.

Verification: full repo suite passes, go test -race ./middleware/adaptor/ clean, golangci-lint v2.12.2 (the version pinned in lint.yml) reports 0 issues.

Not included, deliberately

Two larger wins were measured but left out, since both need a maintainer decision rather than a drive-by:

  1. HTTPHandler is unchanged at 18 allocs/op. All of it is inside fasthttpadaptor.NewFastHTTPHandler, which spawns a goroutine and an io.Pipe per request (~10 allocs) to support Flush/Hijack. Reducing it means forking that concurrency-sensitive upstream code into Fiber.
  2. FiberApp still calls app.Handler() per request, which takes the global app.mutex and runs startupProcess() every time. Hoisting it behind a sync.Once measured −36% on FiberApp_Parallel at 4 cores (scaling 2.1× → 3.3×) — the single largest remaining win. It is not in this PR because it silently drops the implicit tree rebuild for routes registered at runtime, so those would 404 unless the user calls app.RebuildTree(). That is a routing-semantics change and shouldn't ship under a perf banner.

The net/http -> Fiber direction allocated a fresh 32 KiB buffer for every
single request: io.Copy could not use a shortcut because neither
io.LimitedReader nor fasthttp's body writer implements WriterTo/ReaderFrom,
so it fell back to allocating its own buffer. Copy the body with a pooled
buffer and a LimitedReader that lives in the pooled ctx instead, and skip
the copy machinery altogether for http.NoBody.

The response header copy-back allocated a string per key, a string per
value and a one-element slice per header. Pack every key and value into a
single string that the map entries re-slice, and carve the single-valued
entries out of one shared []string, so the copy-back costs two allocations
regardless of how many headers the response carries. Keys still go through
textproto.CanonicalMIMEHeaderKey, so the result matches Header.Add.

Also resolve the "ip:port" remote address into scratch space carried by the
pooled ctx, read the two config fields the handler needs once at
construction (App.Config returns a 624-byte struct by value) and call the
fasthttp adaptor directly in HTTPMiddleware instead of rebuilding
HTTPHandler's wrapper on every request.

Add benchmarks for the previously uncovered HTTPMiddleware and FiberApp
paths, plus tests for the header packing, the shared backing array and the
reused remote address.

    FiberHandlerFunc/No_Content     10326n -> 901n    32986B -> 149B    7 -> 1 allocs
    FiberHandlerFunc_Parallel/1MB    5286n -> 404n    32992B -> 147B    7 -> 1 allocs
    FiberApp                        22608n -> 3606n   33300B -> 1146B  25 -> 12 allocs
    FiberApp_Parallel               13400n -> 942n    33290B -> 404B   14 -> 3 allocs
    FiberApp_Body                   29501n -> 6163n   37436B -> 5229B  21 -> 14 allocs
    HTTPMiddleware                  11719n -> 8793n    2240B -> 2225B  25 -> 24 allocs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQgWGL4o639y2MP1hJjm9E
@gaby
gaby requested a review from a team as a code owner July 26, 2026 19:47
@ReneWerner87 ReneWerner87 added this to v3 Jul 26, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The net/http–Fiber adaptor now uses pooled buffers and scratch storage for request bodies, response headers, and remote addresses. Request population and handler execution are streamlined, configuration is captured once, and tests and benchmarks cover the updated conversion paths.

Changes

Adaptor optimization

Layer / File(s) Summary
Pooled conversion primitives
middleware/adaptor/adaptor.go
Adds pooled request-body buffers, response-header scratch storage, limited readers, and reusable TCP address storage.
Adaptor request and response flow
middleware/adaptor/adaptor.go
Builds fasthttp requests directly, preserves multi-value headers, handles http.NoBody, reuses remote-address storage, captures configuration once, and applies pooled response-header copying.
Adaptor validation and benchmarks
middleware/adaptor/adaptor_test.go
Adds benchmarks and tests for body copying, header copying, address resolution, no-body requests, and remote-address data across pooling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • gofiber/fiber#3078: Refactors the same adaptor path around request-context pooling and HTTP conversion.
  • gofiber/fiber#3477: Optimizes related HTTPHandler and adaptor initialization behavior.
  • gofiber/fiber#4557: Changes related multi-value headers, remote-address handling, and adaptor hot paths.

Suggested labels: ⚡️ Performance

Suggested reviewers: sixcolors, renewerner87, efectn

Sequence Diagram(s)

sequenceDiagram
  participant HTTPClient
  participant HTTPMiddleware
  participant FiberApp
  participant HTTPResponse
  HTTPClient->>HTTPMiddleware: send request and headers
  HTTPMiddleware->>FiberApp: populate fasthttp request and copy limited body
  FiberApp-->>HTTPMiddleware: return response and execution error
  HTTPMiddleware->>HTTPResponse: copy canonical response headers
  HTTPResponse-->>HTTPClient: send adapted response
Loading

Poem

I’m a rabbit with buffers tucked neat,
Reusing each scratch on the street.
Headers hop light,
Addresses stay right,
While body streams race past my feet.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and clearly states the main change: reducing allocations in the net/http adaptor.
Description check ✅ Passed The description largely follows the template and covers purpose, benchmarks, change list, type of change, and checklist items.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/adaptor-middleware-perf-s6o17h

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gaby gaby changed the title perf(adaptor): cut allocations on the net/http bridge ⚡ perf(adaptor): cut allocations on the net/http bridge Jul 26, 2026
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.63636% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.25%. Comparing base (84ecfab) to head (7100a06).

Files with missing lines Patch % Lines
middleware/adaptor/adaptor.go 88.63% 6 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4559      +/-   ##
==========================================
+ Coverage   93.22%   93.25%   +0.03%     
==========================================
  Files         140      140              
  Lines       14685    14747      +62     
==========================================
+ Hits        13690    13753      +63     
+ Misses        622      619       -3     
- Partials      373      375       +2     
Flag Coverage Δ
unittests 93.25% <88.63%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@middleware/adaptor/adaptor_test.go`:
- Around line 1991-1999: Ensure each benchmark iteration uses a fresh
httptest.ResponseRecorder: in middleware/adaptor/adaptor_test.go lines
1991-1999, move w := httptest.NewRecorder() into the b.Loop() body; in lines
2017-2025, create the recorder inside each pb.Next() iteration for every
goroutine. Do not reuse or accumulate recorder state between requests.

In `@middleware/adaptor/adaptor.go`:
- Around line 472-478: Update the HTTP adapter error-call site to invoke
app.ErrorHandler so mounted sub-applications resolve their path-specific
handlers. Keep the cached cfg.BodyLimit/maxBodySize initialization unchanged,
but remove or stop using the cached errorHandler value in adaptor construction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: df0abd9b-47c5-4284-9528-b03decef9d14

📥 Commits

Reviewing files that changed from the base of the PR and between 84ecfab and 6e8321d.

📒 Files selected for processing (2)
  • middleware/adaptor/adaptor.go
  • middleware/adaptor/adaptor_test.go

Comment thread middleware/adaptor/adaptor_test.go Outdated
Comment thread middleware/adaptor/adaptor.go Outdated
Resolving the remote address into storage carried by the pooled ctx made
RemoteAddr()/RemoteIP() unsafe to keep past the handler: the next request
that picked up the same pool entry overwrote it, so code that stashes the
address — an async access log, a connection registry, a stream writer
goroutine — silently reported a later request's client. Three requests from
1.1.1.1, 2.2.2.2 and 3.3.3.3 that each retained the address all ended up
reading 3.3.3.3.

Give every request its own address again, but keep the IP storage inline
next to the net.TCPAddr so the fast path costs one allocation rather than
the two it took before this branch. The IP slice is capped at its length so
appending to it cannot write into the rest of the buffer.

Also give Benchmark_FiberApp_Body and Benchmark_FiberApp_Parallel a fresh
recorder per iteration. Reusing one accumulated the response header map and
body across iterations, which kept copyResponseHeaders permanently on the
"key already present" branch and made FiberApp_Parallel's alloc count
incomparable to the otherwise identical FiberApp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQgWGL4o639y2MP1hJjm9E

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
middleware/adaptor/adaptor.go (1)

579-579: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Call errorHandler as a bare statement.

The error is only used to send the error response, so _ = errorHandler(ctx, err) is unnecessary; use errorHandler(ctx, err) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@middleware/adaptor/adaptor.go` at line 579, Update the error-handling call in
the relevant adaptor flow to invoke errorHandler(ctx, err) directly as a bare
statement, removing the unnecessary blank assignment while preserving the
existing arguments and behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@middleware/adaptor/adaptor.go`:
- Line 579: Update the error-handling call in the relevant adaptor flow to
invoke errorHandler(ctx, err) directly as a bare statement, removing the
unnecessary blank assignment while preserving the existing arguments and
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 80d72d38-dfca-45d7-b20c-8bb6443ddc70

📥 Commits

Reviewing files that changed from the base of the PR and between 6e8321d and 5efde7e.

📒 Files selected for processing (2)
  • middleware/adaptor/adaptor.go
  • middleware/adaptor/adaptor_test.go

Caching cfg.ErrorHandler at construction pinned the root app's handler.
App.ErrorHandler resolves a mounted sub-app's handler from the request
path, so that lookup belongs on the request. BodyLimit stays cached — it is
a plain value and the App.Config copy it avoids is 624 bytes per request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQgWGL4o639y2MP1hJjm9E
@ReneWerner87
ReneWerner87 merged commit 35cba5e into main Jul 27, 2026
20 of 21 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/adaptor-middleware-perf-s6o17h branch July 27, 2026 06:46
@github-project-automation github-project-automation Bot moved this to Done in v3 Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants