curl -fsSL https://bun.sh/install | bash
npm install -g bun
powershell -c "irm bun.sh/install.ps1|iex"
scoop install bun
brew tap oven-sh/bun
brew install bun
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun
bun upgrade
Bun now ships with native headless browser automation built into the runtime. Two backends, one API:
WebKit (macOS default) — uses the system WKWebView. Zero external dependencies.Chrome (cross-platform) — Chrome/Chromium via DevTools Protocol. Auto-detects installed browsers or accepts a custom path.In the next version of Bun
Bun.WebView programmatically controls a headless web browser in Bun pic.twitter.com/Yp8UiNoeoy
All input is dispatched as OS-level events — sites can't distinguish view.click() from a real mouse click (isTrusted: true). Selector-based methods auto-wait for actionability, Playwright-style: the element must be attached, visible, stable, and unobscured before the action fires.
await using view = new Bun.WebView({ width: 800, height: 600 });
await view.navigate("https://bun.sh");
await view.click("a[href="http://bun.zhutiblog.com/com/'/docs']"); // waits for actionability, native click
await view.scroll(0, 400); // native wheel event, isTrusted: true
await view.scrollTo("#install"); // scrolls every ancestor, waits for visible
const title = await view.evaluate("document.title");
const png = await view.screenshot({ format: "jpeg", quality: 90 });
await Bun.write("page.jpg", png);
All methods work across both backends:
MethodDescriptionnavigate(url)Navigate to a URLevaluate(expr)Evaluate JavaScript in the pagescreenshot({format, quality, encoding})Capture a PNG/JPEG/WebP screenshotclick(x, y) / click(selector)Click at coordinates or a CSS selectortype(text)Type text into the focused elementpress(key, {modifiers})Press a key with optional modifiersscroll(dx, dy) / scrollTo(selector)Scroll by delta or to an elementgoBack() / goForward() / reload()Navigation controlsresize(w, h)Resize the viewportcdp(method, params)Raw Chrome DevTools Protocol callview.url / view.title / view.loadingPage state propertiesBun.WebView extends EventTarget — on the Chrome backend, CDP events are dispatched as MessageEvents with the params on event.data. Constructor options include backend ("webkit", "chrome", or { type: "chrome", path, argv }), console to capture page logs, and dataStore for persistent profiles. One browser subprocess is shared per Bun process; additional new Bun.WebView() calls open tabs in the same instance.
Render Markdown in the Terminal with bun ./file.mdYou can now render Markdown files directly in your terminal. When you run bun ./file.md, Bun reads the file, renders it as beautifully formatted ANSI output, and prints it to stdout with no JavaScript VM startup overhead.
In the next version of Bun
bun ./hello.md & Bun.markdown.ansi(string) pretty-prints markdown to terminal-friendly ansi text pic.twitter.com/yp7jYKZL8k
You can also use the new Bun.markdown.ansi() API programmatically:
// Render markdown to an ANSI-colored string
const out = Bun.markdown.ansi("# Hello\n\n**bold** and *italic*\n");
process.stdout.write(out);
// Plain text mode — no escape codes
const plain = Bun.markdown.ansi("# Hello", { colors: false });
// Enable clickable hyperlinks
const linked = Bun.markdown.ansi("[docs](https://bun.sh)", {
hyperlinks: true,
});
// Custom line width for wrapping
const wrapped = Bun.markdown.ansi(longText, { columns: 60 });
// Inline images via Kitty Graphics Protocol (Kitty, WezTerm, Ghostty)
const withImg = Bun.markdown.ansi("", {
kittyGraphics: true,
});
In the next version of Bun
Async stacktraces are supported on native APIs like node:fs, Bun.write, node:http, node:dns & more.
This makes debugging easier pic.twitter.com/PHospWtxtg
Bun.cron now supports an in-process callback overload that runs a function on a cron schedule. This is ideal for long-running servers and containers where you want scheduled work that shares state with the rest of your application.
In the next version of Bun
Bun.cron() accepts a callback for recurring in-process tasks pic.twitter.com/HQ3s3rxGEO
This complements the existing OS-level Bun.cron(path, schedule, title) which registers persistent crontab/launchd/Task Scheduler entries. The in-process variant is lighter, works identically across platforms, and lets your handler access database pools, caches, and module-level state directly.
Key behaviors:
No overlap — the next fire is scheduled only after the handler (and any returned Promise) settles. Slow async work won't pile up concurrent runs.Scheduled in UTC — 0 9 * * * means 9:00 UTC, regardless of the system time zone. (The OS-level Bun.cron(path, schedule, title) variant uses system local time, since that's how crontab/launchd/Task Scheduler work.)Error handling matches setTimeout — synchronous throws emit uncaughtException, rejected promises emit unhandledRejection. Without a listener the process exits with code 1; with one, the job reschedules itself.--hot safe — all in-process cron jobs are cleared before the module graph re-evaluates, so editing the schedule, handler, or removing the call entirely all take effect on save without leaking timers.Disposable — using job = Bun.cron(...) auto-stops at scope exit.ref/unref — .ref() (default) keeps the process alive; .unref() lets it exit naturally.// Error handling example
process.on("unhandledRejection", (err) => console.error("cron failed:", err));
Bun.cron("* * * * *", async () => {
await mightThrow(); // logged, then retried next minute
});
Thanks to @alii for the contribution!
UDP Socket: ICMP Error Handling and Truncation DetectionTwo improvements to Bun.udpSocket() that bring it closer to libuv/Node.js behavior:
ICMP errors no longer silently close the socket. On Linux, sending a UDP packet to an unreachable port previously caused the socket to silently close, breaking all other sends on the same socket. Now, ICMP errors (port unreachable, host unreachable, TTL exceeded, etc.) are surfaced through the error handler, and the socket stays open:
const sock = await Bun.udpSocket({
socket: {
error(err) {
console.log(err.code); // 'ECONNREFUSED'
},
},
});
sock.send("ping", 1, "127.0.0.1"); // dead port — error handler fires, socket stays open
Truncated datagrams are now detectable. When a received datagram is larger than the receive buffer, the kernel silently truncates it. The data callback now receives a fifth flags argument so you can tell truncated payloads from complete ones:
const sock = await Bun.udpSocket({
socket: {
data(socket, data, port, address, flags) {
if (flags.truncated) {
console.log("Datagram was truncated!");
}
},
},
});
Bun's unix domain socket behavior was inverted from Node.js/libuv in two important ways:
Node.js / libuvBun (before)Existing file at bind timeEADDRINUSESilently unlinks and binds anywaySocket file after close()RemovedLeft on diskThis meant Bun could silently steal a live socket from another process on listen(), and leaked .sock files on stop()/close().
Now Bun matches Node.js semantics: binding to an existing socket file correctly returns EADDRINUSE, and closing a listener automatically cleans up the socket file. This applies to Bun.listen, Bun.serve, and net.Server with unix sockets.
import { existsSync } from "node:fs";
const listener = Bun.listen({
unix: "/tmp/my.sock",
socket: { data() {}, open() {} },
});
existsSync("/tmp/my.sock"); // true
listener.stop();
existsSync("/tmp/my.sock"); // false — automatically cleaned up
// Binding to an existing socket now correctly throws EADDRINUSE
// instead of silently taking over the socket
import { listen } from "bun";
const a = listen({ unix: "/tmp/my.sock", socket: { data() {}, open() {} } });
try {
// Previously: silently unlinked and stole the socket
// Now: throws EADDRINUSE, matching Node.js behavior
const b = listen({ unix: "/tmp/my.sock", socket: { data() {}, open() {} } });
} catch (e) {
console.log(e.code); // "EADDRINUSE"
} finally {
a.stop();
}
Bun's underlying JavaScript engine (WebKit's JavaScriptCore) has been upgraded with over 1,650 upstream commits, bringing significant performance improvements, new language features, and bug fixes.
Explicit Resource Management (using and await using)The using and await using declarations from the TC39 Explicit Resource Management proposal are now supported natively in JavaScriptCore:
function readFile(path) {
using file = openFile(path); // file[Symbol.dispose]() called automatically at end of block
return file.read();
}
async function fetchData(url) {
await using connection = await connect(url); // connection[Symbol.asyncDispose]() awaited at end of block
return connection.getData();
}
Thanks to @sosukesuzuki for the contribution!
Improved standalone executables on LinuxStandalone executables created with bun build --compile on Linux now use a proper ELF section (.bun) to embed the module graph, matching the existing approach on macOS and Windows. Previously, the embedded data was read from /proc/self/exe at startup, which failed when the binary had execute-only permissions (chmod 111).
With this change, the kernel maps the data via PT_LOAD during execve — meaning zero file I/O at startup and no read permission required on the binary.
bun build --compile app.ts --outfile myapp
chmod 111 myapp
./myapp # works now on Linux
Thanks to @dylan-conway for the contribution!
URLPattern is up to 2.3x fasterURLPattern.test() and URLPattern.exec() are now significantly faster. The internal regex matching now calls the compiled regex engine directly instead of allocating temporary JavaScript objects for each URL component, eliminating up to 24 GC allocations per call.
BenchmarkBeforeAfterSpeeduptest() match - named groups1.05 µs487 ns2.16xtest() no-match579 ns337 ns1.72xtest() match - simple971 ns426 ns2.28xtest() match - string pattern946 ns434 ns2.18xexec() match - named groups1.97 µs1.38 µs1.43xexec() no-match583 ns336 ns1.73xexec() match - simple1.89 µs1.30 µs1.45xconst pattern = new URLPattern({ pathname: "/api/users/:id/posts/:postId" });
// 2.16x faster
pattern.test("https://example.com/api/users/42/posts/123");
// 1.43x faster
pattern.exec("https://example.com/api/users/42/posts/123");
As a side effect, URLPattern internals no longer pollute RegExp.lastMatch / RegExp.$N — previously, calling pattern.test(url) would leak internal regex state into these legacy static properties.
Thanks to @sosukesuzuki for the contribution!
Faster Bun.stripANSI and Bun.stringWidthSIMD optimizations across Bun.stripANSI, Bun.stringWidth, and the shared ANSI parsing helpers used by Bun.sliceAnsi, Bun.wrapAnsi, and Node's readline.getStringWidth.
Key improvements:
4×-unrolled SIMD prologue for escape character scanning — processes 64 bytes at a time instead of 16, reducing the cost of the NEON→GPR transfer that gates the loop branch.SIMD terminator scans inside ANSI escape parsing — CSI and OSC payloads (like hyperlink URLs) are now skipped in bulk instead of byte-by-byte.Lazy flat buffer allocation in stripANSI — replaces StringBuilder with a raw Vector + memcpy, eliminating per-append bookkeeping and the final shrink-copy.UTF-16 stringWidth escape state machine refactor — long OSC payloads in UTF-16 strings (hyperlinks with emoji) now use bulk SIMD scans instead of per-codepoint stepping.C1 ST (0x9C) recognized as OSC terminator in the Zig stringWidth path, conforming to ECMA-48 and matching the C++ consumeANSI behavior.stripANSI benchmarksInputBeforeAfterImprovementPlain ASCII (1000 chars, no escapes)65.40 ns16.88 ns~4×OSC 8 hyperlink (45 chars)59.57 ns45.12 ns24% fasterBash 150KB78.97 µs71.56 µs9% fasterstringWidth benchmarksInputBeforeAfterImprovementHyperlink + emoji, UTF-16 (440K chars)~2.0 ms180 µs~11×Truecolor SGR (140K chars)~135 µs120 µs10% fasterHyperlink, Latin-1 (445K chars)~135 µs119 µs11% fasterCompared to npm string-width, Bun is 4–822× faster depending on input size and content, and correctly handles all three OSC terminator variants (BEL, ESC \, and C1 ST) where the npm package only recognizes BEL.
Faster bun build on low-core machinesFixed a thread-pool bug that left the bundler running with one fewer worker thread than intended. Most impactful on low-core machines where one thread is a larger share of the pool:
CoresBeforeAfterSpeedup2554–561 ms375–392 ms1.43–1.47×4321 ms301 ms~1.07×16303–316 ms292–296 ms~1.02–1.08×Benchmark: bun build on an 11,669-module project (three.js + @mui/material + @mui/icons-material, 10.45 MB output).
Faster Bun.Glob.scan()Bun.Glob.scan() no longer opens and reads the same directory twice for patterns with a **/X/... boundary (e.g. **/node_modules/**/*.js). Gains scale with how much of the tree sits under the boundary — up to 2x on deeply nested trees. Patterns without a boundary (e.g. **/*.ts) are unchanged.
const glob = new Bun.Glob("**/node_modules/**/*.js");
// This is now up to 2x faster
for await (const path of glob.scan({ cwd: "./my-project" })) {
// ...
}
Additionally, Bun.Glob on Windows now pushes wildcard filters down to the kernel via NtQueryDirectoryFile, so non-matching entries are discarded before reaching userspace — up to 2.4× faster for simple patterns like *.js or pkg-*/lib/*.js in directories with a low match ratio. Patterns using **, ?, [...], or {...} bypass the filter and behave as before.
Cgroup-aware availableParallelism / hardwareConcurrency on LinuxIn the next version of Bun
Threadpool & JIT threads now respect cgroup CPU limits instead of physical cores. This improves resource utilization in Docker & k8shttps://t.co/HCPy7jRCyG
Bun now reuses CONNECT tunnels for HTTPS-through-proxy requests. Previously, every proxied HTTPS request performed a fresh CONNECT handshake and TLS negotiation. Now, the tunnel and inner TLS session are pooled and reused across sequential requests to the same target through the same proxy with the same credentials — matching the behavior of Node.js + undici.
This dramatically reduces latency and connection overhead when making multiple HTTPS requests through a proxy:
// All three requests now reuse a single CONNECT tunnel
// instead of establishing 3 separate tunnels + TLS handshakes
for (let i = 0; i 3; i++) {
const res = await fetch("https://example.com/api", {
proxy: "http://user:pass@proxy.example.com:8080",
});
console.log(res.status);
}
Tunnels are keyed by proxy host/port, proxy credentials, target host/port, and TLS configuration — so different targets or different credentials correctly use separate tunnels.
This also fixes intermittent Malformed_HTTP_Response errors that some users encountered when using fetch with HTTPS proxies.
Thanks to @cirospaciari for the contribution!
TCP_DEFER_ACCEPT for Bun.serve() on LinuxBun.serve() now sets TCP_DEFER_ACCEPT on Linux (and SO_ACCEPTFILTER "dataready" on FreeBSD), the same optimization nginx uses to reduce latency for incoming HTTP connections.
Previously, accepting a new connection required two event loop wake-ups — one for the accept and another to discover the socket was readable. With TCP_DEFER_ACCEPT, the kernel defers the accept until the client has actually sent data (the HTTP request or TLS ClientHello), collapsing the two wake-ups into one:
Before:
epoll wake → accept new socketReturn to epollepoll wake → socket readable → recv() → process → respondAfter:
epoll wake → accept new socket (data already buffered) → recv() → process → respondThis is especially impactful for short-lived connections (e.g. HTTP/1.1 with Connection: close). Bun.listen() and net.createServer() are unchanged, since they may serve protocols where the server sends first. No effect on macOS or Windows.
BugfixesNode.js compatibility improvementsFixed: process.env being completely empty when the current working directory is inside a directory without read permission (e.g., chmod 111). Previously, OS-inherited environment variables passed via execve were lost because the env loader returned early on EACCES before reading process environment variables. (@alii)Fixed: Memory leak where every vm.Script, vm.SourceTextModule, and vm.compileFunction call leaked the resulting object due to a reference cycle in the internal NodeVMScriptFetcher (@sosukesuzuki)Fixed: pipeline(Readable.fromWeb(res.body), createWriteStream(...)) permanently stalling (and eventually spinning at 100% CPU) when piping fetch() response bodies under concurrency, caused by a race between the HTTP thread's body callback and JS accessing res.body (@dylan-conway)Fixed: Readable.prototype.pipe crashing the process when piping an object-mode Readable into a byte-mode Transform/Writable. The ERR_INVALID_ARG_TYPE error is now properly emitted on the destination stream's error event instead of being thrown as an uncatchable exception, matching Node.js behavior.Fixed: node:dns/promises.getDefaultResultOrder being undefined and dns.getDefaultResultOrder() returning the function object instead of a string ("ipv4first" / "ipv6first" / "verbatim"). Also added the missing getServers export to dns.promises. This broke Vite 8 builds under Bun. (@dylan-conway)Fixed: fs.realpathSync("/") throwing ENOENT when running Bun under FreeBSD's Linuxulator compatibility layer or in minimal containers without /proc mounted (@ant-kurt)Fixed: fs.statSync().ino returning INT64_MAX (9223372036854775807) for files with inodes ≥ 2⁶³, causing all files on NFS mounts with high 64-bit inodes to report the same inode number. dev and rdev were also affected. All stat fields now match Node.js behavior for both Stats (Number) and BigIntStats (BigInt) paths. (@dylan-conway)Fixed: process.stdout.end(callback) firing the callback before all data was flushed, causing output truncation at power-of-2 boundaries (64KB, 128KB, etc.) when the callback called process.exit(0)Fixed: Error.captureStackTrace now includes async stack frames (e.g. at async ) matching the behavior of new Error().stack (@Jarred-Sumner)Fixed: a rare crash in Error.captureStackTrace on error objects whose .stack had already been accessed (@Jarred-Sumner)Fixed: assert.partialDeepStrictEqual crashing when comparing arraysFixed: fs.stat, fs.lstat, and fs.fstat throwing EPERM on Linux when running under seccomp filters that block the statx syscall (e.g., older Docker versions EPERM, EINVAL, and abnormal positive return codes from statx.Fixed: fs.Stats(...) called without new scrambled property values — 8 of 10 integer fields (e.g. ino, size, mode) were assigned to the wrong property names due to a slot-order mismatch in the internal constructor path. (@dylan-conway)Fixed: statSync(path) instanceof Stats incorrectly returned false because stat instances used a different prototype object than Stats.prototype. Methods like .isFile() still worked, but identity checks and instanceof did not match Node.js behavior. (@dylan-conway)Updated built-in root TLS certificates to NSS 3.121, the version shipping in Firefox 149. Adds e-Szigno TLS Root CA 2023 and corrects the label for OISTE Server Root RSA G1. (@cirospaciari)Bun APIsFixed: setting process.env.HTTP_PROXY, HTTPS_PROXY, or NO_PROXY (and lowercase variants) at runtime had no effect on subsequent fetch() calls because proxy config was only read once at startup. Changes now take effect on the next fetch(). (@cirospaciari)Fixed: the event loop processing at most 1,024 ready I/O events per tick when more were pending, adding latency on servers handling thousands of concurrent connections. Bun now drains the full backlog in a single tick, matching libuv's uv__io_poll. (@Jarred-Sumner)Fixed: a Bun.serve() performance cliff where concurrent async handlers that resumed after an await couldn't batch their writes (cork buffer contention), dropping throughput from ~190k req/s to ~22k req/s. Also fixes a potential use-after-free where closed sockets could remain referenced in the drain loop. (@Jarred-Sumner)Fixed: a lost-wakeup race in Bun's internal thread pool that could cause fs.promises, Bun.file().text(), Bun.write(), crypto.subtle, and the package manager to hang indefinitely on aarch64 (Apple Silicon, ARM Linux). x86_64 was not affected. (@dylan-conway)Fixed: Memory leak in Bun.serve() when a Promise from the fetch handler never settles after the client disconnects (@Jarred-Sumner)Fixed: Bun.SQL MySQL adapter returning empty results for SELECT queries against MySQL-compatible databases (StarRocks, TiDB, SingleStore, etc.) that don't support the CLIENT_DEPRECATE_EOF capability. Bun now properly negotiates capabilities with the server per the MySQL protocol spec and correctly handles legacy EOF packets.Fixed: per-query memory leaks in the bun:sql MySQL adapter that caused RSS to grow unboundedly until OOM on Linux. Three native allocation leaks were fixed: column name allocations not freed on cleanup or when overwritten during prepared statement reuse, and parameter slice allocations not freed after query execution.Fixed: memory leak in Bun.TOML.parse where the logger's internal message list was not freed on error pathsFixed: Bun.listen() and Bun.connect() could crash with certain invalid hostname or unix values. Now throws a TypeError instead.Fixed: a crash accessing server.url with an invalid unix socket pathFixed: DNS cache entries that were stale but still referenced by in-flight connections would never expire, causing stale DNS results to persist indefinitely (@dylan-conway)Fixed: potential crash in Bun.dns.setServers with certain invalid inputsFixed: Bun.dns.lookup() could crash with certain invalid inputsFixed: Glob scanner crashing or looping infinitely when scanning deeply nested directory trees or self-referential symlinks where the accumulated path exceeds the OS path length limit. Now properly returns an ENAMETOOLONG error instead. (@dylan-conway)Fixed: Unix socket paths longer than 104 bytes (the sun_path limit) now work correctly on macOS. Previously, Bun.serve({ unix }) and fetch({ unix }) would fail with ENAMETOOLONG when the socket path exceeded this limit. (@Jarred-Sumner)Fixed: a crash when reading .fd on a TLS listener created with Bun.listen({ tls })Fixed: crashes in Bun.FFI.linkSymbols() and Bun.FFI.viewSource with invalid symbol descriptors — now throw a TypeError insteadFixed: edge case crash when passing an out-of-range value as a file descriptor to APIs like S3Client.writeWeb APIsFixed: unbounded memory growth from messages sent to a closed MessagePort being queued indefinitely and never delivered. 5000 × 64KB postMessage calls to a closed port dropped RSS from 332MB to 1.5MB. (@sosukesuzuki)Fixed: AbortController.signal.reason silently becoming undefined after garbage collection when only the controller was retained. (@sosukesuzuki)Fixed: use-after-free race in BroadcastChannel when a worker-owned channel was destroyed while another thread looked it up. (@sosukesuzuki)Fixed: CookieMap.toJSON() could crash with numeric cookie namesFixed: String.raw corrupting null bytes (U+0000) in tagged template literals, emitting the 6-character string \uFFFD instead of preserving the original byte. This affected libraries like wasm-audio-decoders that embed WASM binaries as yEncoded strings in template literals.Fixed: AbortSignal memory leak when ReadableStream.prototype.pipeTo is called with a signal option and the pipe never completes. A reference cycle between AbortSignal and its abort algorithm callbacks prevented garbage collection even after all user-side references were dropped. (@sosukesuzuki)Fixed: crash when calling bytes() or arrayBuffer() on a Response whose body was created from an async iterable (Symbol.asyncIterator)Fixed: crash when calling ReadableStream.blob() after the Response body was already consumed, now properly rejects with ERR_BODY_ALREADY_USEDFixed: a crash in Request.formData() / Response.formData() / Blob.formData() when the Content-Type header contained a malformed boundary value (@dylan-conway)Fixed: HTTP server now correctly rejects requests with conflicting duplicate Content-Length headers per RFC 9112, preventing potential request smuggling attacks (@dylan-conway)Fixed: WebSocket connections crashing when headers, URLs, or proxy config contained non-ASCII characters. The upgrade request now correctly decodes all inputs as UTF-8. (@Jarred-Sumner)Fixed: edge case crash formatting error messages when Symbol.toPrimitive throwsFixed: a crash that could occur when a stack overflow happened during error message formattingJavaScript bundlerFixed: bun build --compile on NixOS and Guix producing executables that only ran on the exact same Nix generation, because the compiled binary inherited a /nix/store/... ELF interpreter path. PT_INTERP is now normalized back to the standard FHS path so compiled binaries are portable across Linux systems. (@Jarred-Sumner)Fixed a crash in bun build --compile when CSS files are passed as entry points alongside JS/TS entry pointsbun testFixed: mock.module() could crash when the first argument is not a stringFixed: a crash that could occur when mock.module() triggered auto-install during module resolutionFixed: potential crash in expect.extend with certain invalid inputsFixed: --elide-lines flag no longer exits with an error in non-terminal environments (e.g., CI, Git hooks). The flag is now silently ignored when stdout is not a TTY, allowing the same command to work in both interactive and non-interactive contexts. (@alii)Bun ShellFixed: Bun.$.braces() could crash when called with an empty stringWindowsFixed: tar archive extraction on Windows could write files outside the extraction directory when an entry contained an absolute path (e.g. C:\... or UNC paths) — these entries are now skipped (@dylan-conway)Thanks to 8 contributors!@alii@ant-kurt@cirospaciari@dylan-conway@gameroman@jarred-sumner@robobun@sosukesuzuki