Bun Release: Performance Boosts, CPU Profiling, and Enhanced Stability

Development Tools

Explore the latest Bun release featuring significant performance improvements for `bun install`, a new CPU profiling flag, refined dependency resolution, and numerous bug fixes across Node.js compatibility, N-API, and testing.

This release addresses 287 issues, resulting in 324 positive impacts (πŸ‘).

Installation

To Install Bun

# Using curl (Linux/macOS)
curl -fsSL https://bun.sh/install | bash

# Using npm
npm install -g bun

# Using PowerShell (Windows)
powershell -c "irm bun.sh/install.ps1|iex"

# Using Scoop (Windows)
scoop install bun

# Using Homebrew (macOS/Linux)
brew tap oven-sh/bun
brew install bun

# Using Docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

To Upgrade Bun

bun upgrade

Hoisted Installs Restored as Default

In Bun 1.3.0, isolated installs became the default for workspaces, which aimed to eliminate phantom dependencies and improve install speed. However, this caused issues for existing monorepos relying on shared dependencies.

With Bun 1.3.2, isolated installs are now only the default for new projects. Existing workspaces will revert to using hoisted installs unless explicitly configured otherwise.

To Keep Using Isolated Installs in Existing Workspaces/Monorepos

You can configure this in your bunfig.toml:

[install]
# Explicitly set the linker to isolated
linker = "isolated"

Alternatively, use the --linker=isolated flag:

bun install --linker=isolated

New projects using workspaces (or those without a bun.lock/bun.lockb file) will continue to default to isolated installs.

configVersion Stabilizes Install Defaults

To prevent disruptive changes in future Bun upgrades, bun install now writes a configVersion to your bun.lock or bun.lockb file. This allows future default configuration changes without impacting existing projects.

Here's how it works:

  • New projects: Default to configVersion = 1 (v1). In workspaces, v1 uses the isolated linker by default; otherwise, it uses hoisted linking.
  • Existing Bun projects: If your lockfile doesn't have a version, Bun sets configVersion = 0 (v0) when you run bun install, preserving the previous hoisted linker default.
  • Migrations from other package managers:
    • From pnpm: configVersion = 1 (v1)
    • From npm or yarn: configVersion = 0 (v0)
// Example in bun.lock
{
  // New projects:
  "configVersion": 1,
  // Existing projects without a version (after running `bun install`):
  "configVersion": 0
}

The default linker behavior is now determined by a combination of configVersion and whether workspaces are used:

Lockfile configVersionUsing Workspaces?Default Linker
1βœ…isolated
1❌hoisted
0βœ…hoisted
0❌hoisted

Faster bun install

Projects depending on popular libraries like esbuild or sharp will now experience faster install times.

Future Bun versions will also feature smarter postinstall script execution. For example, a repository with Next.js and Vite saw bun install become 6x faster.

CPU Profiling with --cpu-prof

Bun now supports CPU profiling for any script using the --cpu-prof flag. This feature records detailed information about program execution, helping you identify performance bottlenecks and optimize critical code paths.

The next version of Bun will generate CPU profiles compatible with Chrome DevTools, powered by JavaScriptCore's sampling profiler.

Profiles are saved in the Chrome DevTools-compatible .cpuprofile format and can be opened directly in Chrome DevTools (Performance tab) or VS Code's CPU profiler. Sampling runs at 1ms for fine-grained insights.

CPU Profiling Flags

FlagDescription
--cpu-profEnables profiling
--cpu-prof-name <filename>Sets the output filename
--cpu-prof-dir <dir>Sets the output directory

Example Usage

// script.js
function fib(n) {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
console.log(fib(35)); // some CPU work

To run this script with CPU profiling:

bun --cpu-prof script.js
bun --cpu-prof --cpu-prof-name my-profile.cpuprofile script.js
bun --cpu-prof --cpu-prof-dir ./profiles script.js

Open the generated .cpuprofile file in Chrome DevTools (Performance tab) -> Load profile.

bun:test onTestFinished Hook

bun:test now includes a new onTestFinished(fn) hook. This hook executes at the very end of a test, after all afterEach hooks have completed. It's ideal for cleanup or assertions that must occur post-all other per-test hooks.

  • Runs only inside a test (not in describe or preload).
  • Supports async and done-style callbacks.
  • Not supported in concurrent tests; use test.serial or remove test.concurrent.

Example

import { test, afterEach, onTestFinished, expect } from "bun:test";

test("runs after afterEach", () => {
  const calls = [];
  afterEach(() => {
    calls.push("afterEach");
  });
  onTestFinished(() => {
    calls.push("onTestFinished");
    // afterEach has already run
    expect(calls).toEqual(["afterEach", "onTestFinished"]);
  });
  // test body...
});

test.serial("async cleanup at the very end", async () => {
  onTestFinished(async () => {
    await new Promise((r) => setTimeout(r, 10));
    // ...close DB connections, stop servers, etc.
  });
  // test body...
});

Thanks to @pfg for this contribution!

ServerWebSocket subscriptions Getter

The ServerWebSocket object now features a subscriptions getter. This getter returns a de-duplicated list of topics to which the connection is currently subscribed.

This makes it easier to inspect and manage per-connection state in pub/sub systems, such as debugging topic subscriptions or cleaning up resources when clients disconnect. When a socket closes, subscriptions automatically returns an empty array.

Example

const server = Bun.serve({
  fetch(req, server) {
    if (server.upgrade(req)) return;
    return new Response("Not a websocket");
  },
  websocket: {
    open(ws) {
      ws.subscribe("chat");
      ws.subscribe("notifications");
      console.log(ws.subscriptions); // ["chat", "notifications"]
      ws.unsubscribe("chat");
      console.log(ws.subscriptions); // ["notifications"]
    },
    close(ws) {
      console.log(ws.subscriptions); // []
    },
  },
});

This enhancement significantly improves transparency and debugging for Bun's WebSocket pub/sub model.

Alpine 3.22 in Official Docker Images

Bun's official Alpine Linux Docker images now utilize Alpine 3.22 for both x64 and arm64(musl) builds. This update delivers the latest security patches, improved package compatibility, and a smaller base footprint.

Improved Git Dependency Resolution

bun install now offers enhanced support for npm-style hosted Git URLs and GitHub shorthands. GitHub repositories specified with custom protocol prefixes are now correctly identified and routed through the faster HTTP tarball download pathway.

package.json Example

{
  "dependencies": {
    // GitHub shorthand (now parsed correctly and downloaded via HTTP tarball)
    "cool-lib": "github:owner/repo#v1.2.3",
    // Different protocols resolve deterministically
    "tooling-ssh": "git+ssh://git@github.com/owner/repo.git#main",
    "tooling-https": "git+https://github.com/owner/repo.git#main"
  }
}

Thanks to @markovejnovic for this contribution!

bun list Alias for bun pm ls

You can now list your dependency tree using the shorter, top-level command: bun list. This is a direct alias for bun pm ls and supports all the same flags, including --all for a full transitive view. This makes dependency inspection quicker and easier.

Example Usage

# List dependencies from the current lockfile (alias for `bun pm ls`)
bun list

# Show the full transitive dependency tree
bun list --all

spawnSync Now Runs on an Isolated Event Loop

Bun.spawnSync and child_process.spawnSync now execute on an isolated event loop, separate from the main process. This prevents JavaScript timers and microtasks from firing and interfering with the main process's stdin/stdout, aligning Bun's spawnSync behavior with Node.js. This change also makes timeouts reliable across all platforms, including Windows.

This addresses several bugs and stability issues in the previous implementation, such as execSync with vim "eating" the first character of keypresses. While this improves stability, please report any negative impacts if your project inadvertently relied on the previous behavior.

More Bug Fixes

Node.js Compatibility Improvements

  • EventEmitter: Fixed an error when removeAllListeners(type) was called from within an event handler with a removeListener meta-listener registered and no listeners for the target event. Behavior now matches Node.js (no error).
  • ServerResponse.prototype.writableNeedDrain: Fixed incorrect true return when the response had no handle, which caused fs.createReadStream().pipe(res) and other piped streams to pause indefinitely in middleware scenarios (e.g., Vite static file serving). Behavior now matches Node.js.
  • process.mainModule: Setter/getter semantics now match Node.js.
  • process.nextTick: Fixed crash when user code overrides process.nextTick. Bun now safely uses the overridden function internally.
  • Buffer.isEncoding(''): Now correctly returns false to match Node.js behavior.
  • Module._resolveFilename: Now forwards the options object (including options.paths) to overridden implementations and honors options.paths when provided. This restores compatibility with Node-style require hooks (e.g., Next.js 16) and fixes related build failures.
  • Module._resolveFilename: Now validates that options.paths is an array and throws ERR_INVALID_ARG_TYPE otherwise, aligning with Node.js.
  • process.dlopen: Fixed crash when passed non-object exports (null, undefined, or primitives). Bun now matches Node.js ToObject semantics, preventing segfaults when loading native addons.

N-API and Native Addons

  • napi_create_external_buffer: Now correctly handles empty inputs (null data and/or length 0) without throwing or creating a detached buffer. When length is 0, it returns a detached ArrayBuffer matching Node.js behavior.
  • napi_get_buffer_info & napi_get_arraybuffer_info: Correctly report a null pointer and 0 length, and napi_is_detached_arraybuffer returns true. This prevents crashes in addons and ensures zero-length buffers are created with safe finalization.
  • ThreadSafeFunction Finalizers: Fixed crashes in N-API when ThreadSafeFunction finalizers or async work deinitialized the environment during dispatch. Environment references are now safely retained.
  • N-API Property Access: Now returns undefined for missing properties and out-of-bounds elements, matching Node.js.
  • Numeric-String Keys: Handled consistently as index access across N-API property operations, aligning with Node.js.
  • napi_delete_property, napi_has_property, napi_has_own_property: Now return correct boolean results and propagate exceptions consistently.
  • Error Handling: Improved error handling across N-API property and element access.
  • better-sqlite3: Importing better-sqlite3 now fails with a clear, actionable error (linking to the tracking issue and suggesting bun:sqlite as an alternative) instead of crashing.

HTTP/HTTPS and Networking

  • TLS Verification: Restored use of the system CA trust store for TLS verification, fixing a 1.3.0 regression where some HTTPS requests failed with UNABLE_TO_GET_ISSUER_CERT_LOCALLY. Bun now loads default OS CA paths again.
  • HTTP Server Idle: Fixed an issue where the HTTP server incorrectly marked a connection as idle after a write failure, causing longer than expected timeouts.
  • WebSocket Upgrade CPU: Fixed an issue where upgrading WebSocket connections via the ws module could consume 100% CPU in certain cases when it should be idling.

Fetch API

  • Body Consumption: Fetch API methods now reject with TypeError instead of Error when the body has already been consumed (e.g., calling text() then json() on the same Request/Response), aligning with the Fetch spec and matching Node/Deno behavior.

bun:test Bug Fixes

  • Lifecycle Hooks Options: Fixed bun:test lifecycle hooks (beforeAll, beforeEach, afterAll, afterEach) no longer throwing when called with a callback and options as the second argument. (callback, options) is now correctly parsed, supporting both object and numeric timeouts.
  • Snapshot Creation in CI: bun:test now emits clearer errors when snapshot creation is attempted in CI. Messages explicitly refer to creation, include the received value, and guide users to use --update-snapshots or set CI=false.
  • Test Runner Crashes: Fixed rare crashes when a test prompted for a sudo password or left a dangling process.
  • expect(...).toThrow with Async: Fixed expect(...).toThrow with an async function no longer crashing the test runner when the rejection occurs after the test timeout. The test now times out and reports a failure.
  • vi.mock Types: Fixed bun-types for bun:test incorrectly typing vi.mock(...) as vi.module(...). vi.mock is now correctly typed.

bun build Bug Fixes

  • Sourcemap Sorting: Fixed 2 different sourcemap sorting bugs.

CSS and Styling

  • View-Transition Pseudo-elements: CSS view-transition pseudo-elements now support class selector arguments (e.g., ::view-transition-old(.slide-out)), resolving "Unexpected token: ." errors during parsing/bundling. These selectors now parse, minify, and serialize correctly.
  • @layer Blocks: CSS minifier now processes @layer blocks, ensuring color-scheme rules receive required variable injections and prefers-color-scheme fallbacks for browsers without light-dark() support.

bun install Bug Fixes

  • bun update --interactive: Fixed bun update --interactive (including --latest) updating package.json but not installing selected updates. It now installs updated dependencies and refreshes node_modules.
  • npm: Alias Preservation: bun update --interactive no longer strips npm: alias prefixes when updating dependencies in package.json. Aliases and range operators are preserved.
  • Optional Peer Dependencies: Fixed bun install leaving optional peerDependencies unresolved in isolated installs, which caused inconsistent resolutions and type incompatibilities. Optional peers now resolve to an installed package when available.
  • Git Protocol Conflation: bun install no longer conflates git+ssh and git+https references to the same repository; each specifier is resolved independently.
  • GitHub Custom Protocols: GitHub dependencies with custom protocol prefixes (e.g., git+https://github.com/owner/repo#v1.2.3) are now recognized as GitHub tag downloads, enabling the faster HTTP download path. Improved recognition of GitHub shorthand also increases reliability.

Runtime and Performance

  • Global ~/.bunfig.toml: Fixed global ~/.bunfig.toml being loaded more than once, leading to duplicate configuration. Bun now guarantees it's loaded at most once per run.
  • MySQL OK Packet Parsing: Fixed crash when parsing MySQL OK packets with truncated or empty payloads.
  • Bun.CookieMap#delete: Fixed a crash in Bun.CookieMap#delete in certain cases.
  • ANSI Color Detection: ANSI color support is now detected per stream (stdout vs stderr). This resolves missing colors in errors/crash reports and test diffs, and prevents misrendered box-drawing characters in interactive commands when the terminal doesn't support color.
  • Interactive UI Box Drawing: Interactive UIs and installer output only use box-drawing characters when stdout supports ANSI, avoiding garbled tables in plain terminals.
  • Hot Reload Terminal Clearing: Hot reload terminal-clearing logic respects stdout color capability.
  • Test Framework Output Colors: Test framework output (expect diffs and matcher messages) consistently respects stderr color support.
  • Truncated Stack Traces (glibc): Fixed crash reports on glibc-based Linux showing severely truncated stack traces. Bun now uses Zig's std.debug.captureStackTrace for more complete traces.

Module Resolution

  • ES Module require() with TLA: Fixed requiring an ES module with top-level await via require() or import.meta.require throwing and leaving a partially initialized module in the cache. The failed module is now evicted from the cache.

TypeScript and Types

  • Web Stream Types: TypeScript types for Blob, ReadableStream, and Response now include text(), bytes(), json(), formData(), and arrayBuffer() convenience methods, resolving related type errors.
  • Bun.spawn / spawnSync Options: TypeScript definitions for Bun.spawn and spawnSync now include missing options and match runtime behavior (e.g., detached, onDisconnect, lazy). Also clarified IPC lifecycle ordering with onExit.
  • spawn / spawnSync Option Shapes: spawn/spawnSync option shapes are unified via Bun.Spawn.BaseOptions in the types; Spawn.OptionsObject is deprecated.

Web Crypto

  • crypto.exportKey("jwk"): Fixed crypto.exportKey("jwk") for EC private keys sometimes producing a shorter-than-required "d" parameter, violating RFC 7518. Bun now pads "d" to the correct length (e.g., P-256 (32 bytes), P-384 (48 bytes), P-521 (66 bytes)).

Thanks to Our Contributors!

@alii, @avarayr, @braden-w, @cirospaciari, @csvlad, @dylan-conway, @fraidev, @jarred-sumner, @lillious, @lydiahallie, @markovejnovic, @nektro, @nkxxll, @pfgithub, @riskymh, @robobun, @sosukesuzuki, @taylordotfish