Version
24.19.0
Platform
Microsoft Windows NT 10.0.20348.0 x64
Subsystem
fetch
What steps will reproduce the bug?
Run the test below, with --expose-gc, using different versions of Node.
The expected results are:
Node 24.16 - 24.19 - FAIL: multipart upload does not release chunk buffers after request completion. Bug reproduced.
Node 24.15 and below - PASS: chunk buffers released after request completion.
'use strict'
const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const assert = require('node:assert')
function printMemory(label) {
const memory = process.memoryUsage()
console.log(label)
console.log(` RSS=${Math.round(memory.rss / 1024 / 1024)}MB`)
console.log(` Heap=${Math.round(memory.heapUsed / 1024 / 1024)}MB`)
console.log(` External=${Math.round(memory.external / 1024 / 1024)}MB`)
console.log(` ArrayBuffers=${Math.round(memory.arrayBuffers / 1024 / 1024)}MB`)
}
test(
'multipart upload should release chunk buffers after request completion',
{ timeout: 300000 },
async (t) => {
const server = createServer((req, res) => {
// Fully consume the multipart request body
req.on('data', () => {})
req.on('end', () => {
res.writeHead(200)
res.end()
})
})
t.after(() => server.close())
server.listen(0)
await once(server, 'listening')
const { port } = server.address()
const url = `http://localhost:${port}/upload`
const chunkSize = 5 * 1024 * 1024
const numberOfChunks = 200 // 1GB total
const startMemory = process.memoryUsage()
printMemory('Before upload')
for (let i = 0; i < numberOfChunks; i++) {
const chunkData = Buffer.alloc(chunkSize, 0x41)
const formData = new FormData()
formData.append(
'file_content',
new Blob([chunkData]),
'test.bin'
)
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Range': `bytes ${i * chunkSize}-${(i + 1) * chunkSize - 1}/${numberOfChunks * chunkSize}`,
},
body: formData,
})
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`)
}
await response.text()
// Allow buffers to be collected if they are no longer referenced
if (global.gc) {
global.gc()
}
if ((i + 1) % 20 === 0) {
printMemory(
`After ${(i + 1) * chunkSize / 1024 / 1024}MB uploaded`
)
}
}
const endMemory = process.memoryUsage()
printMemory('After upload')
const arrayBufferGrowth =
endMemory.arrayBuffers - startMemory.arrayBuffers
assert.ok(
endMemory.arrayBuffers - startMemory.arrayBuffers < 200 * 1024 * 1024,
`ArrayBuffer memory grew by ${Math.round(
(endMemory.arrayBuffers - startMemory.arrayBuffers) / 1024 / 1024
)}MB`
)
}
)
How often does it reproduce? Is there a required condition?
It reproduces every time I use Node 24.16 - 24.19
What is the expected behavior? Why is that the expected behavior?
After each upload request completes, the multipart request body should become eligible for garbage collection.
RSS and arrayBuffers should remain approximately constant regardless of the total amount of data uploaded.
What do you see instead?
- Each uploaded chunk appears to remain referenced after the request completes.
- Uploading a 1GB file as 5MB multipart chunks results in approximately 1GB of retained arrayBuffers.
- Heap usage remains stable, but arrayBuffers grows roughly in proportion to the uploaded data.
- Constructing the multipart body manually (without FormData) avoids the issue
Additional information
When repeatedly uploading multipart/form-data requests using FormData and Blob, process RSS and process.memoryUsage().arrayBuffers grow approximately in proportion to the total uploaded data. The memory is not reclaimed after the requests complete, even after forcing garbage collection.
- Same upload logic on Node.js 24.15.0 does not exhibit the behaviour.
- Changing Blob construction did not affect the result.
- The upload wrapper does not retain request objects.
- Replacing FormData with a manually constructed multipart request body immediately resolved the memory growth.
Version
24.19.0
Platform
Subsystem
fetch
What steps will reproduce the bug?
Run the test below, with
--expose-gc, using different versions of Node.The expected results are:
Node 24.16 - 24.19 - FAIL: multipart upload does not release chunk buffers after request completion. Bug reproduced.
Node 24.15 and below - PASS: chunk buffers released after request completion.
How often does it reproduce? Is there a required condition?
It reproduces every time I use Node 24.16 - 24.19
What is the expected behavior? Why is that the expected behavior?
After each upload request completes, the multipart request body should become eligible for garbage collection.
RSS and arrayBuffers should remain approximately constant regardless of the total amount of data uploaded.
What do you see instead?
Additional information
When repeatedly uploading multipart/form-data requests using FormData and Blob, process RSS and process.memoryUsage().arrayBuffers grow approximately in proportion to the total uploaded data. The memory is not reclaimed after the requests complete, even after forcing garbage collection.