Bun 09 ⚡ Building Fast HTTP Servers with Bun.serve()
The Bun.serve() API is the runtime’s built-in HTTP server. It is not a wrapper around Node’s http module, nor does it require an external framework like Express or Fastify. The server is powered by a fork of uWebSockets, written in C++ and Zig, and exposed through a JavaScript API that uses the standard Request and Response objects . The performance claim is concrete: roughly 2.5 times more requests per second than a Node.js http.createServer on Linux for a simple “hello world” response . The API surface is small but includes features that typically require middleware: built-in routing, WebSocket upgrades, TLS, per-request timeouts, and client IP inspection.
Key point: Bun.serve() returns a Server object. The fetch handler receives a Request and returns a Response. The routes object provides built-in routing with path parameters and wildcards. The websocket handler enables WebSocket upgrades. All three can coexist in the same server instance.
Why Bun.serve() exists
The dependency problem. A Node.js HTTP server requires either the built-in http module (which is callback-based and lacks routing) or a framework like Express (which adds a dependency tree). Bun.serve() provides the server, the router, and the WebSocket handler in the runtime itself. A complete API server can be written in a single file without npm install .
The performance problem. Node’s http module is implemented in JavaScript, and its request/response cycle involves multiple object allocations. Bun’s server is implemented in native code and uses uWebSockets for the HTTP parsing and connection handling. The benchmark in Bun’s documentation shows the gap: approximately 160,000 requests per second for Bun.serve() versus 64,000 for Node’s http on the same hardware .
The routing problem. A server without routing requires manual URL parsing. Bun.serve() includes a tree-based router with SIMD-accelerated parameter decoding. Routes are defined as an object: the keys are path patterns (/users/:id), and the values are handler functions or static Response objects. The router matches in order of specificity: exact routes first, then parameter routes, then wildcards, then the global catch-all .
The WebSocket problem. A Node.js WebSocket server requires the ws package or uWebSockets.js. Bun.serve() includes WebSocket upgrade handling and a handler object with open, message, close, and drain callbacks. The server.upgrade() method upgrades the connection, and the websocket handler takes over. Topics and backpressure are built in .
The trade-off. Bun.serve() is optimized for Bun. It does not run on Node.js. The Request and Response objects are the standard Fetch API types, but the server-specific features—server.upgrade(), server.timeout(), server.requestIP()—are Bun-only. A project that needs to run on both runtimes cannot use these features.
a. The basic server and fetch handler
The simplest server defines a fetch function that returns a Response. The function receives a Request and the Server instance as arguments.
const server = Bun.serve({
port: 3000,
fetch(req, server) {
return new Response("Hello from Bun!");
},
});
console.log(`Listening on ${server.url}`);
The server.url property is a URL object containing the protocol, hostname, and port. The server.port property is the actual port (useful when port: 0 is passed to let the OS choose). The server.protocol is "http" or "https" .
The fetch handler runs for every request that is not matched by a route. If a routes object is also defined, the router attempts to match first, and fetch handles the fallthrough .
b. Routing with routes
The routes object maps path patterns to handlers. A handler can be a function that returns a Response (or a Promise<Response>), or a static Response object that is returned without invoking a function .
const server = Bun.serve({
routes: {
"/": () => new Response("Home"),
"/health": new Response("OK"),
"/api/users/:id": (req) => {
return Response.json({ id: req.params.id });
},
"/files/*": (req) => {
return new Response(`File path: ${req.params["*"]}`);
},
"/*": () => new Response("Not found", { status: 404 }),
},
fetch() {
return new Response("Fallback");
},
});
Path parameters are accessed through req.params. The type of params is inferred from the route string when it is passed as a string literal, so req.params.id is recognized by TypeScript without a cast . The wildcard * captures the remainder of the path and is available as params["*"].
The route precedence is strict: exact routes (/users/all) are checked before parameter routes (/users/:id), which are checked before wildcards (/users/*), which are checked before the global catch-all (/*) . A request to /users/all matches the exact route, not the parameter route.
Static Response objects are optimized for zero-allocation dispatch. "/health": new Response("OK") does not invoke a function for each request; the same Response object is reused .
c. WebSocket handling
The websocket handler defines the callbacks for WebSocket connections. The server.upgrade() method upgrades an HTTP request to a WebSocket and passes contextual data to the handler.
const server = Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/chat") {
const upgraded = server.upgrade(req, {
data: { username: url.searchParams.get("name") },
});
if (upgraded) return undefined;
return new Response("Upgrade failed", { status: 400 });
}
return new Response("Hello");
},
websocket: {
data: {} as { username: string },
open(ws) {
ws.subscribe("chat");
ws.send("Welcome to the chat");
},
message(ws, message) {
ws.publish("chat", `${ws.data.username}: ${message}`);
},
close(ws) {
ws.unsubscribe("chat");
},
},
});
The data property in the websocket handler is the type declaration for ws.data. The actual value is passed to server.upgrade() and is available in every handler . The ws.subscribe() and ws.publish() methods implement a topic-based pub/sub system. A message published to a topic is sent to every subscriber except the sender (unless publishToSelf is enabled) .
The message handler receives a string or a Buffer/ArrayBuffer/Uint8Array depending on the binaryType setting. The ws.send() method returns a number: -1 for backpressure, 0 for a dropped message, and the byte count for a successful send .
Complete Example Session
# ============================================
# PART 1: BASIC SERVER
# ============================================
const server = Bun.serve({
port: 3000,
fetch() {
return new Response("Hello");
},
});
console.log(`Listening on ${server.url}`);
# ============================================
# PART 2: ADD ROUTES
# ============================================
const server = Bun.serve({
routes: {
"/": () => new Response("Home"),
"/api/status": () => Response.json({ ok: true }),
"/users/:id": (req) => Response.json({ id: req.params.id }),
},
fetch() {
return new Response("Not found", { status: 404 });
},
});
# ============================================
# PART 3: STATIC RESPONSE
# ============================================
routes: {
"/health": new Response("OK"),
}
# ============================================
# PART 4: WILDCARD ROUTE
# ============================================
routes: {
"/files/*": (req) => new Response(`Path: ${req.params["*"]}`),
}
# ============================================
# PART 5: WEBSOCKET UPGRADE
# ============================================
fetch(req, server) {
if (new URL(req.url).pathname === "/ws") {
const upgraded = server.upgrade(req, { data: { id: 1 } });
if (upgraded) return undefined;
return new Response("Upgrade failed", { status: 400 });
}
return new Response("Not found", { status: 404 });
}
# ============================================
# PART 6: WEBSOCKET HANDLER
# ============================================
websocket: {
data: {} as { id: number },
message(ws, message) {
ws.send(`Echo: ${message}`);
},
}
# ============================================
# PART 7: PER-REQUEST TIMEOUT
# ============================================
fetch(req, server) {
server.timeout(req, 60);
return new Response("Long request allowed");
}
# ============================================
# PART 8: CLIENT IP
# ============================================
fetch(req, server) {
const ip = server.requestIP(req);
return new Response(`IP: ${ip?.address}`);
}
# ============================================
# PART 9: TLS
# ============================================
const server = Bun.serve({
tls: {
key: Bun.file("./key.pem"),
cert: Bun.file("./cert.pem"),
},
fetch() {
return new Response("Secure");
},
});
The nine parts trace the server lifecycle: basic server, routing, static responses, wildcards, WebSocket upgrade, WebSocket handlers, per-request timeout, client IP inspection, and TLS.
Quick Reference
Server Options
| Option | Purpose |
|---|---|
port | Port to listen on (default 3000) |
hostname | Bind address (default “0.0.0.0”) |
fetch | Fallback request handler |
routes | Route map (static, param, wildcard) |
websocket | WebSocket handler object |
tls | TLS configuration (key, cert) |
development | Enable dev error messages |
Server Methods
| Method | Purpose |
|---|---|
server.upgrade(req, opts) | Upgrade to WebSocket |
server.timeout(req, seconds) | Override idle timeout |
server.requestIP(req) | Get client IP and port |
server.reload(opts) | Hot-reload handlers |
server.stop() | Stop the server |
server.ref() / server.unref() | Control process keep-alive |
Server Properties
| Property | Purpose |
|---|---|
server.url | Full URL |
server.port | Actual port |
server.protocol | “http” or “https” |
server.pendingRequests | In-flight request count |
server.pendingWebSockets | Connected WebSocket count |
WebSocket Handler
| Callback | Purpose |
|---|---|
open(ws) | Connection established |
message(ws, msg) | Message received |
close(ws, code, reason) | Connection closed |
drain(ws) | Backpressure relieved |
Best Practices
✅ Do This:
routes: {
"/health": new Response("OK"),
}
<!-- Use static Response for health checks --> # ✅
server.upgrade(req, { data: { userId } });
<!-- Pass contextual data to WebSocket handler --> # ✅
server.timeout(req, 0);
<!-- Disable timeout for Server-Sent Events --> # ✅
❌ Don’t Do This:
# Assume fetch handler runs for matched routes
<!-- Router matches first; fetch handles fallthrough --> # ❌
# Use server.upgrade() without checking the return value
<!-- Upgrade may fail; handle the false case --> # ❌
# Rely on pendingRequests for precise metrics
<!-- It is a snapshot, not a rate counter --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Route not matching | Precedence rules | Place exact routes before parameter routes |
| WebSocket upgrade fails | Request not checked | Handle the false return from server.upgrade() |
| Streaming response closed | Default idle timeout | server.timeout(req, 0) |
req.params undefined | Route has no parameters | Only use params on parameterized routes |
| TLS not working | Key/cert are paths, not contents | Use Bun.file() to read contents |
Real-World Examples
1. Basic Server
Bun.serve({ fetch: () => new Response("Hi") });
2. JSON Route
routes: { "/api": () => Response.json({ ok: true }) }
3. Parameter Route
routes: { "/users/:id": (req) => Response.json({ id: req.params.id }) }
4. Wildcard Route
routes: { "/files/*": (req) => new Response(req.params["*"]) }
5. WebSocket Upgrade
server.upgrade(req, { data: { id: 1 } })
6. WebSocket Handler
websocket: { message(ws, msg) { ws.send(msg) } }
7. Client IP
server.requestIP(req)?.address
8. Per-Request Timeout
server.timeout(req, 60)
9. TLS
tls: { key: Bun.file("key.pem"), cert: Bun.file("cert.pem") }
10. Hot Reload
server.reload({ fetch: () => new Response("v2") })
Visual
Request Flow
┌──────────────────────────────────────────────┐
│ Incoming Request │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Router │
│ ├── Exact route? → Handler │
│ ├── Parameter route? → Handler │
│ ├── Wildcard? → Handler │
│ └── Catch-all? → Handler │
└──────────────────┬───────────────────────────┘
│ No match
▼
┌──────────────────────────────────────────────┐
│ fetch handler │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Response │
└──────────────────────────────────────────────┘
WebSocket Upgrade
┌──────────────────────────────────────────────┐
│ fetch(req, server) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ server.upgrade(req, { data }) │
└──────────┬───────────────────────┬───────────┘
│ true │ false
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ WebSocket handler │ │ Return HTTP Response│
│ open/message/close │ │ (upgrade failed) │
└──────────────────────┘ └──────────────────────┘
Summary
| Item | Value |
|---|---|
| Server constructor | Bun.serve({ ... }) |
| Basic handler | fetch(req, server) |
| Router | routes: { "/path": handler } |
| Path parameters | req.params |
| WebSocket upgrade | server.upgrade(req, { data }) |
| WebSocket handler | websocket: { open, message, close } |
| TLS | tls: { key, cert } |
| Client IP | server.requestIP(req) |
| Per-request timeout | server.timeout(req, seconds) |
| Performance claim | ~2.5x Node.js http |
Key takeaways:
Bun.serve()is the runtime’s HTTP server. It does not require Express or Fastify. The server, router, and WebSocket handler are built in .- The router is tree-based and precedence-ordered. Exact routes match before parameter routes, which match before wildcards. The
fetchhandler catches unmatched requests . - WebSocket upgrades pass contextual data. The
dataobject passed toserver.upgrade()is available asws.datain every handler . - TLS is configured with key and cert contents. The
Bun.file()helper reads the file contents. Passing a path string does not work . - The performance advantage is measurable. Bun’s benchmark shows roughly 2.5x more requests per second than Node’s
httpmodule on Linux .
Remember: Bun.serve() is not a framework. It is the server. The fetch handler is the fallback; the routes object is the router; the websocket object is the WebSocket handler. The server is the process. The port is the entry point. The Response is the output.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!