This document is repository policy for Bun APIs that have shown native memory retention under sustained Streams workloads. Treat it as authoritative when changing object-store, fetch-body, file-body, ingest, or background indexing code.
Do not introduce high-volume use of Bun APIs that materialize native-backed
Blob, File, S3File, ArrayBuffer, or request/response bodies without a
specific memory investigation.
Prefer explicit streaming APIs and bounded byte budgets. If a code path must materialize bytes, it must be protected by all of the following:
- a documented size bound
- bounded concurrency
- memory-sampler coverage for RSS, anon RSS, heap, external, and arrayBuffers
- a local 1 GiB Linux-container stress test when the path can run in production
Forced Bun.gc(true) is not an acceptable mitigation by itself. The observed
failure mode is RSS, especially anon RSS, staying high while JS heap and
arrayBuffers are much lower.
Avoid these APIs in long-lived, high-volume production paths:
Bun.S3ClientBun.S3FileBun.S3File.arrayBuffer()Response.arrayBuffer()over repeated remote downloadsResponse.blob()over repeated remote downloadsBun.file(path).arrayBuffer()Bun.file(path).bytes()Bun.file(path).text()for repeated large filesBun.file(path)as a fetch upload body in sustained object-store upload paths
Small tests, CLI utilities, and bounded local-only helpers may still use these APIs, but production server paths must use extra care. If in doubt, treat the API as unsafe until a memory sampler run proves otherwise.
For object-store uploads:
- use signed
fetch()requests instead ofBun.S3Client - stream file uploads with
node:fs.createReadStream()converted throughnode:stream.Readable.toWeb() - keep upload concurrency bounded by the memory preset
- avoid hidden follow-up reads or stats unless required for correctness
For object-store reads:
- prefer ranged reads or streaming reads
- use
Response.body.getReader()rather thanResponse.arrayBuffer() - if the object-store interface must return
Uint8Array, collect chunks from the stream reader under a known object/range size limit
For local files:
- prefer
node:fsstreams for large or repeated reads - use
Bun.mmap()only for immutable cache files whose pinned mapping is intentionally tracked as a cache/leak-candidate budget - avoid repeated
Bun.file().arrayBuffer(),bytes(), ortext()loops over large files in the server
For HTTP request bodies:
- keep append bodies capped by
DS_APPEND_MAX_BODY_BYTES - keep ingest concurrency and queue bytes bounded
- on low-memory presets, close append keep-alive connections and keep the post-append GC path throttled and observable
The production symptom was repeated Compute OOM kills during external event ingestion. The generator was not colocated with the Streams server, so the memory pressure belonged to the Streams server process and its background segment/upload/index work.
Production failures had this shape:
- process killed around
809 MiBanon RSS plus about51 MiBshmem RSS - JS heap, external memory, and tracked application counters did not explain the RSS high water
- the host clamped a nominal
1024 MBpreset to about684.9 MiBof internal pressure headroom before the kernel killedbun
Local reproduction and fixes:
- MockR2 with
300msoperation latency alone did not reproduce the OOM shape. A 500k-event run peaked around340 MBRSS and290 MBanon RSS. - The R2-compatible path using Bun's native S3 implementation against MinIO did
reproduce production-shaped pressure. A 900k-event, 1 GiB Linux-container run
reached about
850.9 MBRSS and834.4 MBanon RSS during background companion catch-up. - Replacing
Bun.S3Client/S3Filewith signedfetch()R2 requests dropped the same class of run to about533.8 MBRSS and506.1 MBanon RSS. - Removing the remaining
Bun.file(path)upload body andResponse.arrayBuffer()R2 reads reduced the streamed R2 path further. A 900k-event, 1 GiB Linux-container run peaked at about474.8 MBRSS and461.2 MBanon RSS, with cgroupmemory.peakabout637.1 MB, and settled near204.7 MBRSS and188.9 MBanon RSS.
Interpretation: R2 latency can increase overlap between upload and background
work, but the decisive local reproduction came from Bun native S3/body
materialization. Avoiding the Bun S3 API and avoiding remaining Blob/File
arrayBuffer paths materially reduced anon RSS.
These issues were open or still relevant when this document was written on 2026-04-25. Re-check status before removing any guardrail.
- oven-sh/bun#29083:
Bun.S3File.arrayBuffer()retains RSS and reaches OOM in a 1 GiB Linux container despite forced GC. This is the closest public repro to the Streams R2 failure. - oven-sh/bun#28741:
fetch
Blob/ArrayBuffermemory is not reclaimed after references are cleared and GC is forced. - oven-sh/bun#20487:
large file downloads through
@google-cloud/storageand Bun S3 keep RSS high after GC; the reporter observed Node returning closer to baseline while Bun accumulated RSS. - oven-sh/bun#28427: simple repeated fetch polling report marked as a Bun memory leak / needs triage.
- oven-sh/bun#15020:
repeated file reads with
node:fsandBun.Filereported as memory not being freed. - oven-sh/bun#12941: earlier Blob/ArrayBuffer GC-retention report. This one was closed as not planned, but it is relevant history because later open reports describe the same retention class.
Before merging a change that touches body, file, fetch, or object-store code, check:
- Does the change add
Bun.S3Client,Bun.S3File,Bun.file(),.blob(), or.arrayBuffer()to a hot path? - If bytes are materialized, what is the maximum size and concurrency?
- Is the memory visible in
GET /v1/server/_memorDS_MEMORY_SAMPLER_PATHoutput? - Has the path been tested in a memory-limited Linux container when it can run on Compute?
- If RSS/anon RSS remains high after work completes, did heap, external,
arrayBuffers, SQLite stats, active jobs, ingest queue bytes, and index or companion phases explain it?
If the answer is unclear, use the streaming alternative first.