September 4, 2026·4 min read·engineeringtypescriptbunpackaginghttp

Bun 1.4.1 makes offline installation a strict policy

Offline and prefer-offline express different network policies in Bun 1.4.1. The same release adds HTTP/2 to Bun.serve while preserving its request handler interface.

A dependency cache can be a performance preference or a required installation input. Bun 1.4.1, released on September 4, gives those choices different flags. --offline forbids network requests and fails when a required cached package is unavailable. --prefer-offline uses cached metadata regardless of its age but still downloads missing packages.

That difference makes the feature more useful than a general promise of faster installs. A CI job restoring a cache can require the installation to succeed from those inputs. A developer with an unreliable connection can instead prefer the cache while permitting a fallback. The chosen policy determines what a missing input means.

TypeScript's ecosystem is a large part of why I value it, despite my reservations about the language. The tooling around that ecosystem should make build assumptions visible. A cache that silently changes from an input to an optimization can make the same installation command behave differently across environments.

Offline does not mean merely avoiding refreshes

The release explains the ordinary metadata behavior. Bun can refresh a cached package's metadata when it expires. Prefer-offline skips that age-based refresh and accepts the cached copy. A package not present locally can still require network access.

Strict offline mode has a stronger contract. The installer makes no network requests. If the restored cache lacks a required package, the release's example reports the missing cached manifest by name. The error identifies an incomplete installation input instead of hiding it behind an online fetch.

The configuration forms preserve the distinction. install.offline = true selects the strict policy, while install.prefer = "offline" expresses the preference. Putting either setting in configuration changes the default for subsequent invocations, so the name needs to communicate the intended behavior accurately.

The installation documentation treats the cache and lock file as separate concerns. Cached entries supply available artifacts and metadata, while the lock records the selected dependency versions. Frozen-lockfile behavior prevents the installer from replacing that recorded selection and reports a disagreement between the project configuration and its dependency record.

An offline run without a stable selection and a locked run without cached artifacts answer different questions. One constrains where inputs may come from. The other constrains which dependencies installation should select. Combining the appropriate policies can make both assumptions explicit in automation.

This distinction also clarifies a failed build. A cache miss indicates that the offline inputs are incomplete. A lock disagreement indicates that the dependency record does not match the requested project state. Those are different defects in build preparation, even if both prevent installation from finishing.

HTTP/2 uses the existing request interface

The same release adds HTTP/2 support to Bun.serve(). HTTP/1.1 and HTTP/2 can share the port, routes, and fetch handler. The release says req.url and req.body behave identically across those protocols.

This lets an application retain its handler while enabling another transport protocol. The release demonstrates the opt-in through the http2 setting. A minimal cleartext example keeps the response logic independent of protocol selection.

Bun.serve({
  http2: true,
  fetch(request) {
    return new Response(new URL(request.url).pathname)
  },
})

Over TLS, the server negotiates the protocol through ALPN. On a cleartext connection, the release says a client sending the HTTP/2 preface receives HTTP/2 service. A separate http1: false option refuses HTTP/1.x clients. Enabling HTTP/2 and requiring HTTP/2 are therefore different choices too.

That is a useful interface boundary. The handler describes the application's response, while the server configuration describes acceptable protocols. A deployment can support both clients without duplicating routes merely to account for the transport.

The release also states limits. WebSockets and response trailers are not yet supported over HTTP/2. A shared handler interface does not imply that every surrounding protocol capability is identical. Applications that use those capabilities need to account for the particular connection on which they are available.

Integration should retain meaningful choices

The two additions concern different layers, but both expose a policy that a generic convenience setting could obscure. Installation distinguishes a cache preference from a ban on network access. Serving distinguishes accepting HTTP/2 from refusing HTTP/1.x. The implementation can share work while configuration preserves the decision that belongs to the application.

That is the part of Bun's integrated approach I find useful. A smaller amount of application glue is valuable when the remaining settings name real behavior. A flag that states a network prohibition gives automation something concrete to depend on, while an HTTP setting can expand client support without restructuring request handling.

For a build using the offline flag, the cache must be prepared as an input whose absence is allowed to fail the job. For a server using HTTP/2, the existing handler remains the application boundary, subject to the documented feature limits. Bun 1.4.1 makes those choices explicit in the operations that consume them.