Streaming a file response with Bun.file(), no extra deps
Published September 11, 2026
import { serve, file } from "bun";
const server = serve({
async fetch(req) {
const pathname = new URL(req.url).pathname;
const asset = file(`./public${pathname}`);
if (!(await asset.exists())) return new Response("Not found", { status: 404 });
return new Response(asset);
},
});
console.log(`Serving ./public at ${server.url}`);What
Bun.file(path) returns a BunFile — a lazy, Blob-shaped reference to a file on disk.
Passing it straight into new Response(asset) streams the file to the client as it's read,
instead of loading it into memory first.
Why it matters
The obvious approach in Node — fs.readFile then res.end(buffer) — reads the entire file
into memory before sending a single byte, which is wasteful for anything large and adds latency
proportional to file size before the response even starts. Bun's BunFile is intentionally
lazy: constructing it does no I/O at all, and new Response(file) hands the file straight to
the platform's fastest available syscall for the job (sendfile/copy_file_range on Linux,
fcopyfile/clonefile on macOS) instead of routing bytes through JavaScript. No streaming
library, no manual ReadableStream plumbing.
How it works
file(...)builds aBunFilereference — no disk access happens yet, so building the path string is cheap even for a request that turns out to be a 404.asset.exists()is the one async check we do need:BunFilecan point at a path that doesn't exist, and.size/.typeon a missing file just read as0and a default MIME type instead of throwing.new Response(asset)is where the actual streaming happens. Bun setsContent-Typefrom the file's detected type and reads the file directly into the response body.
Gotchas
Bun.file()never throws for a missing path by itself — only.exists(),.text(), etc. surface the absence. Skipping the.exists()check means the client sees a confusing 200 with an empty body instead of a clean 404.- The MIME type is guessed from the file extension. For extensionless files or unusual types,
pass a second argument —
Bun.file(path, { type: "application/octet-stream" })— to override it. - This snippet does no path sanitization; a request path is concatenated directly onto a disk
path. Reaching into
..segments needs to be blocked before this is exposed to untrusted input — see the static-file-router post for a fallback pattern that guards against it.
Related
Bun.write(destination, data)is the inverse — write a string,Blob, or anotherBunFileto disk with the same fast-path syscalls.- Serving a static file as a
routesvalue ("/logo.png": Bun.file("./logo.png")) skips thefetchhandler entirely for paths known at server-start time.