Inside Bun v1.4: Native HTTP/2, Zero-Allocation Streaming I/O, and WebSocket Backpressure
The open-source JavaScript runtime ecosystem continues to evolve rapidly, pushing beyond raw startup microbenchmarks toward production-grade systems engineering. The release of Bun v1.4.1 and subsequent maintenance release v1.4.2 mark a significant architectural leap for the Bun open-source project.
While early releases focused on package management velocity and drop-in Node.js API compatibility, the v1.4 cycle tackles long-standing server primitives: transport-level HTTP/2 multiplexing on unified ports, kernel-friendly streaming disk I/O, true client-side WebSocket backpressure, and aggressive garbage-collection strategies for idle processes.
This deep dive examines how these features work under the hood, how to leverage them in production architectures, and the engineering trade-offs they introduce.
1. Unified HTTP/2 and HTTP/1.1 Multiplexing in Bun.serve()
Historically, supporting HTTP/2 in Node.js applications required either running a dedicated node:http2 server (which uses an entirely different, stream-oriented API distinct from node:http) or offloading HTTP/2 termination to reverse proxies like NGINX or Envoy.
Bun v1.4 brings first-class HTTP/2 support directly into Bun.serve(), allowing a single server instance to handle both HTTP/1.1 and HTTP/2 requests transparently over the same port.
import { serve } from "bun";
serve({
port: 3000,
tls: {
cert: Bun.file("./certs/server.crt"),
key: Bun.file("./certs/server.key"),
},
http2: true, // Enables HTTP/2 alongside HTTP/1.1
fetch(req) {
const url = new URL(req.url);
const protocol = req.headers.get("x-forwarded-proto") ?? "https";
return Response.json({
status: "ok",
path: url.pathname,
protocol: req.headers.get(":scheme") ? "h2" : "http/1.1",
});
},
});
Transport Mechanics and ALPN Negotiation
When TLS is configured, Bun.serve() negotiates the application layer protocol using Application-Layer Protocol Negotiation (ALPN). Clients supporting h2 establish multiplexed HTTP/2 streams over a single TCP connection, while legacy clients fall back to http/1.1 seamlessly.
For unencrypted (cleartext) traffic, Bun inspects incoming data for the HTTP/2 connection preface (PRI * HTTP/2.0\r \r SM\r \r ). If present, the connection transitions to HTTP/2 without prior knowledge; otherwise, it handles standard HTTP/1.x requests. If you run strictly in an internal service mesh where only HTTP/2 is desired, setting http1: false rejects legacy clients immediately.
Architectural Benefit
Because req.url, req.body, and standard Web Request/Response abstractions behave identically regardless of protocol, application handlers remain completely agnostic of the underlying transport layer.
2. Kernel-Direct Streaming Disk Writes via Bun.write()
Buffering large HTTP downloads or file uploads into heap memory before flushing them to persistent storage is a primary source of out-of-memory (OOM) crashes in containerized microservices.
In Bun v1.4, Bun.write() natively accepts standard Web Response, Request, or ReadableStream instances and streams bytes directly to disk rather than buffering the entire payload into user-space memory:
// Previous pattern: High memory pressure (buffers payload into heap)
// const res = await fetch("https://cdn.example.com/large-archive.tar.gz");
// const buf = await res.arrayBuffer();
// await Bun.write("./archive.tar.gz", buf);
// Bun v1.4 streaming pattern: Direct kernel-level chunk pipeline
const response = await fetch("https://cdn.example.com/large-archive.tar.gz");
if (!response.ok || !response.body) {
throw new Error(`Failed to download: ${response.statusText}`);
}
// Memory footprint remains flat regardless of download size
const bytesWritten = await Bun.write("./archive.tar.gz", response);
console.log(`Successfully streamed ${bytesWritten} bytes to disk`);
According to the Bun v1.4.1 release benchmarks, streaming a 128 MiB archive directly to disk reduced peak Resident Set Size (RSS) overhead from 161 MB down to 13 MB—over a 90% reduction in memory overhead.
Additionally, upstream fetch() and S3 client downloads now implement proactive backpressure: reads automatically pause whenever 256 KiB of the body remains unread, preventing upstream network sockets from flooding local process memory.
3. WebSocket Client Backpressure: pause() and resume()
The WHATWG WebSocket specification was designed for browser environments, where socket lifecycle and buffer draining are largely abstracted. However, in backend microservices processing real-time market data feeds, telemetry pipelines, or message brokers, incoming message volume can easily outpace database write speeds or processing capacity.
Without transport backpressure, messages queue up indefinitely in heap memory until the process terminates with an allocation failure.
Bun v1.4 addresses this limitation by introducing socket.pause() and socket.resume() to its native WebSocket implementation:
import { createWriteStream } from "node:fs";
const fileStream = createWriteStream("./telemetry.log", { flags: "a" });
const socket = new WebSocket("wss://stream.example.org/telemetry");
socket.addEventListener("message", (event) => {
const data = typeof event.data === "string"
? event.data
: Buffer.from(event.data);
// If the downstream file buffer is full, apply TCP backpressure
const canContinue = fileStream.write(data + "
");
if (!canContinue) {
socket.pause(); // Stops reading from the underlying TCP socket
fileStream.once("drain", () => {
socket.resume(); // Resumes reading once file buffer has flushed
});
}
});
socket.addEventListener("close", () => {
fileStream.end();
});
How TCP Backpressure Propagates
- Calling
socket.pause()halts read syscalls on the underlying OS socket. - The local kernel's TCP receive window fills up.
- The local TCP stack transmits zero-window or reduced-window advertisements to the remote peer.
- The sending server automatically pauses packet transmission until
socket.resume()flushes the pipeline. - In addition,
socket.bufferedAmountnow accurately tracks outbound bytes awaiting transmission, enabling symmetric upstream and downstream flow control.
4. JIT-Inlined Buffer I/O Operations
High-throughput network protocols and binary serializers (such as Protocol Buffers, FlatBuffers, and custom telemetry codecs) spend substantial CPU cycles reading and writing binary integers and floats.
In Bun v1.4, JavaScriptCore's Just-In-Time (JIT) compiler now inlines methods on Buffer and Uint8Array directly into bounds-checked machine instructions:
const buffer = Buffer.allocUnsafe(16);
// Under Bun v1.4 JIT, these compile directly into single load/store machine instructions
buffer.writeFloatLE(3.14159, 0); // ~9.2x faster than v1.4.0 (0.31ns vs 2.85ns)
buffer.writeUInt8(0xFF, 4); // ~7.2x faster than v1.4.0 (0.31ns vs 2.24ns)
const val = buffer.readUInt32BE(0); // ~1.8x faster than v1.4.0 (0.42ns vs 0.76ns)
By eliminating function call dispatch overhead and foreign function interface (FFI) boundary penalties, intensive binary parsing workloads see notable latency reductions.
5. Idle JIT Code Purging for Long-Running Microservices
A common challenge when operating JavaScript runtimes in cloud microservices is memory bloat caused by JIT compilation artifacts. Long-running web applications (such as Next.js SSR servers or API gateways) often compile extensive code paths during startup and peak traffic bursts, retaining megabytes of JIT machine code long after traffic subsides.
Bun v1.4 modifies JavaScriptCore's memory management to detect extended idle periods and systematically discard non-critical JIT-generated machine code. If traffic spikes again, critical paths are simply re-JITted on demand.
| Application Runtime | Bun v1.3.14 | Bun v1.4.0 | Bun v1.4.1 | Node.js v26 |
|---|---|---|---|---|
| Next.js SSR | 1,303 MB | 222 MB | 142 MB | 195 MB |
| Hono HTTP API | 53 MB | 35 MB | 34 MB | 92 MB |
| Fastify | 78 MB | 65 MB | 55 MB | 89 MB |
| Express | 76 MB | 65 MB | 53 MB | 83 MB |
Memory measurements represent process Resident Set Size (RSS) after 60 seconds of sustained load followed by 3 minutes of idle time on Linux x64.
6. Tree-Shaking Advancements in bun build
For developers utilizing Bun's integrated bundler (bun build), v1.4 resolves a critical limitation with barrel files and namespace exports (export * as).
Previously, importing a single sub-module from packages structured with wildcard namespace objects (such as zod, effect, or fp-ts) caused the bundler to retain the entire module graph. Bun v1.4 compiles these references into direct function bindings, enabling rigorous dead-code elimination:
// schema.ts
import { z } from "zod";
// Bun v1.4 compiles z.object and z.string directly, eliminating
// 240+ unused Zod internal classes, helpers, and locale bundles
export const UserSchema = z.object({
id: z.string(),
});
- In production minified builds, a basic Zod schema bundle dropped from 375.3 KB down to 77.3 KB—a 79% reduction in bundled client JavaScript.
- Dynamic imports (
const { isOdd } = await import("./math")) now also participate in static dead-code elimination, pruning unreferenced sibling exports from lazy-loaded chunks.
Key Considerations and Limitations
Before migrating production services to Bun v1.4's newer primitives, keep the following operational caveats in mind:
- HTTP/2 Features in Progress: While
Bun.serve()supports HTTP/2 request/response streaming, HTTP/2 Server Push and HTTP/2 trailers are currently not supported. If your system depends on trailers (e.g., specific gRPC framing), continue using dedicated gRPC transports. - WebSocket Pause Scope:
socket.pause()is a proprietary Bun runtime extension; it is not part of the browser WHATWG standard. Client-side browser code sharing this logic must abstract or guard calls topause()andresume(). - Re-JIT Latency Trade-Off: Idle memory pruning reduces server cost in multi-tenant environments, but the first request following an extended idle lull may experience a slight microsecond JIT re-compilation delay.
Summary
Bun v1.4 demonstrates how modern open-source runtimes are evolving from raw benchmark competitors into sophisticated, low-footprint systems platforms. With unified HTTP/2 support, zero-allocation disk streaming, socket backpressure, and intelligent idle memory reclamation, backend TypeScript services can achieve greater resource density and reliability in containerized deployments.