Skip to content

Clone fetch response buffer to main thread heap for proper GC reclamation#28743

Open
robobun wants to merge 2 commits intomainfrom
farm/2bc12f00/fix-fetch-body-memory-leak
Open

Clone fetch response buffer to main thread heap for proper GC reclamation#28743
robobun wants to merge 2 commits intomainfrom
farm/2bc12f00/fix-fetch-body-memory-leak

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Apr 1, 2026

Problem

Fetch response body memory is never returned to the OS after GC collects the Blob/Response objects (#28741, #12941, #28427).

Reproduction:

let blobs = [];
for (let i = 0; i < 30; i++) {
  const res = await fetch(url);
  blobs.push(await res.blob());
}
blobs = [];
Bun.gc(true); // Memory NOT freed — 0% released

Node.js releases ~73% of memory in the same scenario; Bun released 0%.

Root Cause

FetchTasklet.callback() runs on the HTTP thread, where it appends response data to scheduled_response_buffer via write(). This allocates the buffer on the HTTP thread's mimalloc heap.

When the body is later consumed and GC finalizes the Blob, Store.deref()Bytes.deinit() calls mi_free() on the main JS thread. In mimalloc, cross-thread frees go to the allocating thread's delayed-free list. Since the HTTP thread is idle after fetches complete, it never processes that list — so the pages are never returned to the OS.

Manual new Blob([data]) works fine because the data is allocated on the main thread from the start.

Fix

In cloneResponseBufferToMainThread(), when transferring the response buffer from FetchTasklet to the body value (which always happens on the main thread in onBodyReceived/toBodyValue), clone the data into a fresh main-thread allocation. The old HTTP-thread buffer is freed immediately.

Verification

Before After
System bun (30×1MB fetch→blob→GC) 0.0% released
Debug build (same test) 28.5% released

The test spawns a local server, fetches 30×1MB blobs, drops all references, forces GC, and asserts >20% memory is reclaimed. Passes with the fix, fails on system bun.

Closes #28741

…tion

The scheduled_response_buffer in FetchTasklet is written to on the HTTP
thread, so its backing memory is allocated on the HTTP thread's mimalloc
heap. When this memory is later freed from the main JS thread during GC
finalization (Blob.finalize → Store.deref → Bytes.deinit), mimalloc puts
the freed block on the HTTP thread's delayed-free list. Since the HTTP
thread is idle after fetches complete, it never processes that list, so
the pages are never returned to the OS.

Fix: when transferring the response buffer to the body value on the main
thread (in onBodyReceived and toBodyValue), clone the data into a fresh
allocation on the main thread's mimalloc heap. The old HTTP-thread buffer
is freed immediately (cross-thread free, goes to delayed list, but is
small overhead). The body data now lives on the main thread's heap, so
GC finalization properly reclaims it.

Closes #28741
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Apr 1, 2026

Updated 5:34 AM PT - Apr 1st, 2026

@autofix-ci[bot], your commit 8772e0b has 4 failures in Build #43090 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28743

That installs a local version of the PR into your bun-28743 executable, so you can run:

bun-28743 --bun

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ff4dc92d-7d33-424a-97a7-d17f0ba4eafc

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed4186 and 8772e0b.

📒 Files selected for processing (2)
  • src/bun.js/webcore/fetch/FetchTasklet.zig
  • test/regression/issue/28741.test.ts

Walkthrough

The changes modify fetch response body buffering in FetchTasklet.zig to use a dedicated cloneResponseBufferToMainThread() helper that duplicates buffered bytes on the main thread and resets the buffer, ensuring proper memory management. A regression test validates that fetch response body memory is reclaimed by garbage collection.

Changes

Cohort / File(s) Summary
Fetch buffer cloning
src/bun.js/webcore/fetch/FetchTasklet.zig
Added cloneResponseBufferToMainThread() helper to duplicate buffered response bytes on the main thread and reset the buffer. Updated buffering completion path and body value construction to use cloned byte list instead of directly transferring ownership of scheduled response buffer.
Memory GC regression test
test/regression/issue/28741.test.ts
Added test that spawns a subprocess with an HTTP server returning 1MB buffers, performs multiple fetch requests, collects blobs, triggers garbage collection, and verifies that at least 20% of allocated memory is reclaimed.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: cloning fetch response buffer to main thread heap for GC reclamation, which is the core fix for the memory leak issue.
Description check ✅ Passed The description comprehensively covers Problem, Root Cause, Fix, and Verification sections, exceeding the basic template requirements with detailed technical explanations and test results.
Linked Issues check ✅ Passed The code changes (cloning response buffer on main thread and adding GC reclamation test) directly address issue #28741's requirement: memory allocated during fetches should be reclaimed by GC when references are removed.
Out of Scope Changes check ✅ Passed All changes are scoped to addressing the fetch response buffer memory leak: the FetchTasklet buffer cloning implementation and corresponding regression test for GC memory reclamation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blob/ArrayBuffer memory not reclaimed by GC after dereferencing

1 participant