Bun

Bun v1.1.10


Jarred Sumner · May 24, 2024

Bun v1.1.10 is here! This release fixes 20 bugs. 2x faster uncached bun install on Windows. fetch() uses up to 2.8x less memory. Several bugfixes to bun install, sourcemaps, Windows reliability improvements and Node.js compatibility improvements

We're hiring systems engineers in San Francisco to build the future of JavaScript!

Previous releases

  • v1.1.9 fixes 67 bugs (addressing 150 👍). Fixes to: workspaces in bun install, sourcemaps in bun build, IPv6 & VPN connectivity, Loading UNC paths, junctions, symlinks, and pnpm on Windows, fetch() gets faster, added dns.prefetch() API, atob() gets 8x faster, toString('base64url') gets 5x faster, expect().toBeReturned() matcher, Node.js compatibility improvements, Bun Shell fixes, and lots more bugfixes.
  • v1.1.8 fixes 54 bugs (addressing 184 👍). Support for process.on("uncaughtException") and process.on("unhandledRejection"), JSON.parse gets faster, Brotli support in node:zlib, [Symbol.dispose] in Bun APIs, fixes lots of crashes on Windows, and many other bugfixes.
  • v1.1.0 Bundows. Windows support is here!

To install Bun

curl
npm
powershell
scoop
brew
docker
curl
curl -fsSL https://bun.sh/install | bash
npm
npm install -g bun
powershell
powershell -c "irm bun.sh/install.ps1|iex"
scoop
scoop install bun
brew
brew tap oven-sh/bun
brew install bun
docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

To upgrade Bun

bun upgrade

fetch uses less memory

fetch() gets smarter about when the response body is no longer in use, and releases the memory sooner.

512 KB responses (unconsumed)

In the following code, Bun v1.1.10 uses 2.8x less memory than Bun v1.1.7, and 3.6x less memory than Node v22.

const server = process.argv.at(-1);
const fmt = new Intl.NumberFormat();
let total = 0;
const batch = 50;
const delay = 32;
while (total < 20_000) {
  for (let i = 0; i < batch; i++) {
    fetch(server);
  }
  await new Promise((r) => setTimeout(r, delay));
  total += batch;
}

console.log(
  "RSS",
  (process.memoryUsage.rss() / 1024 / 1024) | 0,
  "MB after",
  fmt.format((total += batch)) + " fetch() requests",
);

After 20,050 requests on a macOS arm64 machine:

RuntimeMemory usage
Bun v1.1.10166 MB
Bun v1.1.7467 MB
Node v22601 MB

512 KB responses (consuming arrayBuffer())

Consuming the body also releases memory sooner. In the following code, Bun v1.1.10 uses 1.6x less memory than Bun v1.1.7, and 10x less memory than Node v22.

const server = process.argv.at(-1);
const fmt = new Intl.NumberFormat();
let total = 0;
const batch = 50;
const delay = 32;
while (total < 20_000) {
  for (let i = 0; i < batch; i++) {
    fetch(server).then((r) => r.arrayBuffer());
  }
  await new Promise((r) => setTimeout(r, delay));
  total += batch;
}

console.log(
  "RSS",
  (process.memoryUsage.rss() / 1024 / 1024) | 0,
  "MB after",
  fmt.format((total += batch)) + " fetch() requests",
);

After 20,050 requests on a macOS arm64 machine:

RuntimeMemory usage
Bun v1.1.10285 MB
Bun v1.1.7469 MB
Node v221911 MB

Huge thanks to @cirospaciari for this improvement!

How we made fetch use less memory

JavaScriptCore's garbage collector lets native code hold weak and strong references to JavaScript objects. fetch() is especially complicated because it involves:

  • Promise, which must be kept alive until the Response is fulfilled.
  • Response, which must be kept alive at least until the headers and status code become available.
  • ReadableStream, which, if read, must be kept alive while the body is being downloaded.
  • Buffered data - Response can buffer data into a Uint8Array, ArrayBuffer, text, json, formData, or blob, which must be kept alive while the body is being read.

First, this sets a hard constraint on Promise - the Promise object must be kept alive at least until we receive the HTTP status code and headers. That means it's a JSC::Strong. The easy thing to do would be to stop here and just keep the Response and it's body alive until the Response is fulfilled and the HTTP response is completely read. This is the approach we took in Bun v1.1.9 and earlier (before this release).

Once we have the headers and status code, we can release the Promise's strong reference. From there, we need to track the lifetime of the Response object. We only need to keep the Response object alive as long as it is observable to JavaScript, if the response body is being read. JavaScriptCore's JSC::Weak let's us attach a finalizer to a JSC::JSCell (a JavaScript object). This finalizer function is called after the JSC::JSCell is no longer reachable from JavaScript, which is how we know when the Response object is no longer observable to JavaScript.

However, we need to hold a JSC::Weak value to the Response object, but we cannot access the Response object itself once the finalizer is called - the JavaScript object is already freed by then. So the combination of JSC::Strong and JSC::Weak is not enough to get us all the way here. We need to continue to be able to access the Response object after it's no longer accessible to JavaScript, so that we can handle the ReadableStream and the buffered data.

For this, we turned to a common manual memory management approach: reference counting. When there's still pending buffered or streaming data from fetch, we increment the reference count on the Response object in Zig. When the garbage collector calls Response's finalizer, it decrements its reference count. If that reference count is zero, then it frees the Response body and the Response object.

When the garbage collector calls fetch's finalizer notifying it that the Response object has been collected, we now know if the body will no longer be accessible and can tell the HTTP client to ignore the response body.

2x faster uncached bun install on Windows

On Windows, bun install gets a 2x speedup when resolving package versions.

PS C:\bun> hyperfine "bun install --ignore-scripts" "bun-1.1.8 install --ignore-scripts" --prepare="del /s /q bun.lockb && del /s /q C:\Users\window\.bun\install\cache" --warmup=1
Benchmark 1: bun install --ignore-scripts
  Time (mean ± σ):      1.343 s ±  0.398 s    [User: 0.321 s, System: 0.178 s]
  Range (min … max):    0.830 s …  1.861 s    10 runs

Benchmark 2: bun-1.1.8 install --ignore-scripts
  Time (mean ± σ):      3.997 s ±  0.204 s    [User: 0.264 s, System: 0.192 s]
  Range (min … max):    3.753 s …  4.409 s    10 runs

Summary
  bun install --ignore-scripts ran
    2.98 ± 0.89 times faster than bun-1.1.8 install --ignore-scripts

We will write a blog post on the technical details of this improvement soon.

Fixed: Regression in v1.1.9 with hanging bun install on Windows

You know how back in the year 2009, non-blocking I/O was a new thing? Well, bun users on Windows experienced a small version of what the time before non-blocking I/O was like.

bun install would sometimes hang indefinitely when resolving package versions while the system was under load or the network was flaky. This problem existed earlier on Windows, but was exacerbated in v1.1.9 due to implementing a subset of Happy Eyeballs. The issue was that we were not making the TCP client connection non-blocking until after the socket was already connected. This also caused blocking I/O when connecting via fetch, Bun.connect(), new WebSocket(). It did not impact Bun.serve() or Bun.listen().

Fixing this also led to a 20% performance improvement to bun install on Windows, compared to v1.1.8 (before the regression).

Fixed: ENOENT parsing package.json error

A regression introduced in Bun v1.1.9 where bun add <package-name> inside a subfolder within a workspace package would fail in certain cases has been fixed, thanks to @dylan-conway.

Fixed: package-lock.json migration with workspaces fix

Bun supports automatically migrating from package-lock.json -> bun.lockb, but an error could occur when that package-lock.json contained workspace packages. This has been fixed, thanks to @dylan-conway.

Fixed: Crash after modifying "overrides" or "resolutions"

A crash that could occur on the next bun install after modifying the overrides or resolutions fields in package.json has been fixed, thanks to @gvilums.

New: setDefaultTimeout in bun:test

bun:test now supports the setDefaultTimeout function to modify the default timeout for tests in the current scope or module.

import { test, setDefaultTimeout } from "bun:test";

// Timeout after 10 milliseconds:
setDefaultTimeout(10);

test("timeout", async () => {
  await Bun.sleep(9999999);
});

Previously, you'd have to set the timeout for each test individually:

import { test } from "bun:test";

test("timeout", async () => {
  await Bun.sleep(9999999);
}, 10);

You can still set the timeout for each test individually.

For Jest compatibility, we've also implemented jest.setTimeout in bun:test.

import { test, jest } from "bun:test";

jest.setTimeout(10);

test("timeout", async () => {
  await Bun.sleep(9999999);
});

We chose to name bun:test's version "setDefaultTimeout" instead of "setTimeout" to avoid confusion with the global setTimeout timer function.

Thanks to @dylan-conway for this feature!

Fixed: Potential hang in spawned processes on macOS

A bug where spawned processes could hang indefinitely while reading from stdout, stderr, or stdin has been fixed.

Why did this happen?

On macOS, calling send(2) with the MSG_NOWAIT flag with a blocking socket still blocks. On Linux, MSG_NOWAIT causes it to not block. So if you managed to send a very large amount of data to a socket, the parent process would potentially block.

Unlike pipes, marking one end of a socket as non-blocking is not observable to the other end.

To verify this, we can compile the following C program:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
  // Sleep for 10 seconds so that the other process has time to mark stdout as
  // non-blocking
  sleep(10);

  // Check if stdout is blocking or non-blocking
  int stdout_flags = fcntl(fileno(stdout), F_GETFL);
  if (stdout_flags & O_NONBLOCK) {
    printf("stdout is non-blocking\n");
  } else {
    printf("stdout is blocking\n");
  }

  // Check if stderr is blocking or non-blocking
  int stderr_flags = fcntl(fileno(stderr), F_GETFL);
  if (stderr_flags & O_NONBLOCK) {
    printf("stderr is non-blocking\n");
  } else {
    printf("stderr is blocking\n");
  }

  // Check if stdin is blocking or non-blocking
  int stdin_flags = fcntl(fileno(stdin), F_GETFL);
  if (stdin_flags & O_NONBLOCK) {
    printf("stdin is non-blocking\n");
  } else {
    printf("stdin is blocking\n");
  }

  return 0;
}

And then spawn it in Bun:

import { spawn, $ } from "bun";

await $`cc -o a.out a.c`;

const { stdout } = spawn({
  cmd: ["a.out"],
  stdout: "pipe",
  stderr: "pipe",
  stdin: "pipe",
});

console.log(await new Response(stdout).text());

The output will be:

stdout is blocking
stderr is blocking
stdin is blocking

This output is correct because blocking stdout, stderr, and stdin are necessary for many UNIX programs to work correctly. For example, cat will not work correctly if stdout is non-blocking and returns EAGAIN. We want Bun to not have blocking stdout, stderr, and stdin, but it's very important that programs relying on blocking stdout, stderr, and stdin continue to work correctly.

So we can safely mark the socket as non-blocking without affecting the other end.

To prevent future regressions, we've added a regression test that writes & reads large amounts of data to another process in the right conditions to cause this error, and verified that this test previously failed on macOS.

Fixed: Sourcemaps invalid JSON with --splitting in bun build

A bug where sourcemaps in certain cases were not generating a correct JSON object when using --splitting in bun build has been fixed, thanks to @paperdave. This bug was caused by incorrectly joining sourcemap strings from separate files. To prevent future regressions, we've improved our test coverage to verify sourcemaps are valid.

More improvements to sourcemaps in Bun are coming soon.

Windows fixes

Fixed: 1s delay to Worker exit

An event loop bug caused a 1s delay to Worker exit on Windows. This has been fixed, thanks to @gvilums.

Backslashes in import paths in bun build

A bug where an unescaped backslash could be inserted in bun build's import specifiers has been fixed, thanks to @paperdave.

Fixed: bun --watch not killing instances of Bun

Previously, when using bun --watch, instances of Bun could be left running after exiting. This issue has been fixed, thanks to @paperdave.

Node.js compatibility improvements

Fixed: EventEmitter in streams bug

A bug where event listeners removed within an emit call would be skipped has been fixed. This bug impacted node:stream, but did not impact node:events. Thanks to @gvilums for fixing this.

This bug impacted Postgres.js when closing connections.

Crash report uploading

On macOS & Linux, Bun will attempt to automatically upload bun.report links to our crash reporting service when a crash occurs. This will help us fix bugs faster. You can disable this by setting BUN_CRASH_REPORTER_URL="". Please continue to report crashes to us on GitHub, as it really helps us fix bugs. Learn more bun.report.

On Linux, this is enabled in the canary build of Bun but not on the release build.

Thanks to 9 contributors!