Skip to main content

Basic Setup

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bindex.ts
const server = Bun.serve({
  // `routes` requires Bun v1.2.3+
  routes: {
    // Static routes
    "/api/status": new Response("OK"),

    // Dynamic routes
    "/users/:id": req => {
      return new Response(`Hello User ${req.params.id}!`);
    },

    // Per-HTTP method handlers
    "/api/posts": {
      GET: () => new Response("List posts"),
      POST: async req => {
        const body = await req.json();
        return Response.json({ created: true, ...body });
      },
    },

    // Wildcard route for all routes that start with "/api/" and aren't otherwise matched
    "/api/*": Response.json({ message: "Not found" }, { status: 404 }),

    // Redirect from /blog/hello to /blog/hello/world
    "/blog/hello": Response.redirect("/blog/hello/world"),

    // Serve a file by lazily loading it into memory
    "/favicon.ico": Bun.file("./favicon.ico"),
  },

  // (optional) fallback for unmatched routes:
  // Required if Bun's version < 1.2.3
  fetch(req) {
    return new Response("Not Found", { status: 404 });
  },
});

console.log(`Server running at ${server.url}`);

HTML imports

Import HTML files directly into your server code to build full-stack applications with both server-side and client-side code. HTML imports work in two modes: Development (bun --hot): Bun bundles assets on demand at runtime and enables hot module replacement (HMR): when you change your frontend code, the browser updates without a full page reload. Production (bun build): When you build with bun build --target=bun, the import index from "./index.html" statement resolves to a pre-built manifest object containing all bundled client assets. Bun.serve serves the assets from this manifest with no bundling at runtime.
import myReactSinglePageApp from "./index.html";

Bun.serve({
  routes: {
    "/": myReactSinglePageApp,
  },
});
HTML imports don’t just serve HTML: they run Bun’s bundler, JavaScript transpiler, and CSS parser, so you can build frontends with React, TypeScript, and Tailwind CSS. For a complete guide to building full-stack applications with HTML imports, see fullstack dev server.

Configuration

Changing the port and hostname

To configure which port and hostname the server listens on, set port and hostname in the options object.
Bun.serve({
  port: 8080, // defaults to $BUN_PORT, $PORT, $NODE_PORT otherwise 3000
  hostname: "mydomain.com", // defaults to "0.0.0.0"
  fetch(req) {
    return new Response("404!");
  },
});
To randomly select an available port, set port to 0.
const server = Bun.serve({
  port: 0, // random port
  fetch(req) {
    return new Response("404!");
  },
});

// server.port is the randomly selected port
console.log(server.port);
Read the chosen port from the server’s port or url property.
console.log(server.port); // 3000
console.log(server.url); // http://localhost:3000

Configuring a default port

Several flags and environment variables set the default port, which Bun uses when the port option is not set.
  • --port CLI flag
bun --port=4002 server.ts
  • BUN_PORT environment variable
BUN_PORT=4002 bun server.ts
  • PORT environment variable
terminal
PORT=4002 bun server.ts
  • NODE_PORT environment variable
terminal
NODE_PORT=4002 bun server.ts

Unix domain sockets

To listen on a unix domain socket, pass the unix option with the path to the socket.
Bun.serve({
  unix: "/tmp/my-socket.sock", // path to socket
  fetch(req) {
    return new Response(`404!`);
  },
});

Abstract namespace sockets

On Linux, Bun also supports abstract namespace sockets: prefix the unix path with a null byte.
Bun.serve({
  unix: "\0my-abstract-socket", // abstract namespace socket
  fetch(req) {
    return new Response(`404!`);
  },
});
Unlike unix domain sockets, abstract namespace sockets are not bound to the filesystem and are automatically removed when the last reference to the socket is closed.

HTTP/3 (QUIC)

HTTP/3 support in Bun.serve is experimental and may change in future releases.
Bun.serve can also listen for HTTP/3 over QUIC. Set http3: true together with tls; HTTP/3 requires TLS.
Bun.serve({
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
  http3: true, 
  fetch(req) {
    return new Response("Hello over HTTP/3!");
  },
});
When http3 is enabled, the server listens on the same port over both TCP (HTTP/1.1) and UDP (HTTP/3). HTTP/1.1 responses include an Alt-Svc header advertising the HTTP/3 endpoint so capable clients can upgrade automatically. To serve HTTP/3 only — no TCP listener at all — set http1: false:
Bun.serve({
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
  http3: true,
  http1: false, 
  fetch(req) {
    return new Response("HTTP/3 only");
  },
});
http3 is not supported with unix domain sockets — QUIC requires a UDP port. http1: false requires http3: true.

idleTimeout

By default, Bun.serve closes connections after 10 seconds of inactivity. A connection is idle when no data is being sent or received, including in-flight requests where your handler is still running but hasn’t written any bytes to the response yet. Browsers and fetch() clients see this as a connection reset. To configure this, set the idleTimeout field (in seconds). The maximum value is 255, and 0 disables the timeout entirely.
Bun.serve({
  // 30 seconds (default is 10)
  idleTimeout: 30,

  fetch(req) {
    return new Response("Bun!");
  },
});
Streaming & Server-Sent Events — The idle timer applies while a response is being streamed. If your stream goes quiet for longer than idleTimeout, Bun closes the connection mid-response. For long-lived streams, disable the timeout for that request with server.timeout(req, 0).

export default syntax

Instead of passing the server options into Bun.serve, you can export default them.
server.ts
import type { Serve } from "bun";

export default {
  fetch(req) {
    return new Response("Bun!");
  },
} satisfies Serve.Options<undefined>;
The type parameter <undefined> is the WebSocket data type. If you add a websocket handler that attaches custom data with server.upgrade(req, { data: ... }), replace undefined with your data type. You can run this file as-is: when Bun sees a file with a default export containing a fetch handler, it passes it into Bun.serve.

Hot Route Reloading

Update routes without server restarts using server.reload():
const server = Bun.serve({
  routes: {
    "/api/version": () => Response.json({ version: "1.0.0" }),
  },
});

// Deploy new routes without downtime
server.reload({
  routes: {
    "/api/version": () => Response.json({ version: "2.0.0" }),
  },
});

Server Lifecycle Methods

server.stop()

To stop the server from accepting new connections:
const server = Bun.serve({
  fetch(req) {
    return new Response("Hello!");
  },
});

// Gracefully stop the server (waits for in-flight requests)
await server.stop();

// Force stop and close all active connections
await server.stop(true);
By default, stop() allows in-flight requests and WebSocket connections to complete. Pass true to immediately terminate all connections.

server.ref() and server.unref()

Control whether the server keeps the Bun process alive:
// Don't keep process alive if server is the only thing running
server.unref();

// Restore default behavior - keep process alive
server.ref();

server.reload()

Update the server’s handlers without restarting:
const server = Bun.serve({
  routes: {
    "/api/version": Response.json({ version: "v1" }),
  },
  fetch(req) {
    return new Response("v1");
  },
});

// Update to new handler
server.reload({
  routes: {
    "/api/version": Response.json({ version: "v2" }),
  },
  fetch(req) {
    return new Response("v2");
  },
});
Use this for development and hot reloading. Only fetch, error, and routes can be updated.

Per-Request Controls

server.timeout(Request, seconds)

Override the idle timeout for an individual request. Pass 0 to disable the timeout entirely for that request.
const server = Bun.serve({
  async fetch(req, server) {
    // Give this request up to 60 seconds of inactivity instead of the default 10
    server.timeout(req, 60);

    // If they take longer than 60 seconds to send the body, the request will be aborted
    await req.text();

    return new Response("Done!");
  },
});
Use server.timeout(req, 0) to keep a long-lived streaming response (like Server-Sent Events) alive without raising the global idleTimeout for every request:
Bun.serve({
  routes: {
    "/events": (req, server) => {
      // Disable the idle timeout for this streaming response.
      // Otherwise the connection will be closed if no bytes
      // are sent for 10 seconds (the default idleTimeout).
      server.timeout(req, 0);

      return new Response(
        async function* () {
          yield "data: hello\n\n";
          // events can arrive sporadically without the connection being killed
        },
        { headers: { "Content-Type": "text/event-stream" } },
      );
    },
  },
});

server.requestIP(Request)

Get client IP and port information:
const server = Bun.serve({
  fetch(req, server) {
    const address = server.requestIP(req);
    if (address) {
      return new Response(`Client IP: ${address.address}, Port: ${address.port}`);
    }
    return new Response("Unknown client");
  },
});
Returns null for closed requests or Unix domain sockets.

Server Metrics

server.pendingRequests and server.pendingWebSockets

Monitor server activity with built-in counters:
const server = Bun.serve({
  fetch(req, server) {
    return new Response(
      `Active requests: ${server.pendingRequests}\n` + `Active WebSockets: ${server.pendingWebSockets}`,
    );
  },
});

server.subscriberCount(topic)

Get count of subscribers for a WebSocket topic:
const server = Bun.serve({
  fetch(req, server) {
    const chatUsers = server.subscriberCount("chat");
    return new Response(`${chatUsers} users in chat`);
  },
  websocket: {
    message(ws) {
      ws.subscribe("chat");
    },
  },
});

Benchmarks

The following Bun and Node.js servers respond Bun! to each incoming Request.
Bun
Bun.serve({
  fetch(req: Request) {
    return new Response("Bun!");
  },
  port: 3000,
});
require("http")
  .createServer((req, res) => res.end("Bun!"))
  .listen(8080);
The Bun.serve server can handle roughly 2.5x more requests per second than Node.js on Linux.
RuntimeRequests per second
Node 16~64,000
Bun~160,000
image

Practical example: REST API

Here’s a basic database-backed REST API using Bun’s router with zero dependencies:
import type { Post } from "./types.ts";
import { Database } from "bun:sqlite";

const db = new Database("posts.db");
db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT NOT NULL
  )
`);

Bun.serve({
  routes: {
    // List posts
    "/api/posts": {
      GET: () => {
        const posts = db.query("SELECT * FROM posts").all();
        return Response.json(posts);
      },

      // Create post
      POST: async req => {
        const post: Omit<Post, "id" | "created_at"> = await req.json();
        const id = crypto.randomUUID();

        db.query(
          `INSERT INTO posts (id, title, content, created_at)
           VALUES (?, ?, ?, ?)`,
        ).run(id, post.title, post.content, new Date().toISOString());

        return Response.json({ id, ...post }, { status: 201 });
      },
    },

    // Get post by ID
    "/api/posts/:id": req => {
      const post = db.query("SELECT * FROM posts WHERE id = ?").get(req.params.id);

      if (!post) {
        return new Response("Not Found", { status: 404 });
      }

      return Response.json(post);
    },
  },

  error(error) {
    console.error(error);
    return new Response("Internal Server Error", { status: 500 });
  },
});

Reference

See TypeScript Definitions
interface Server extends Disposable {
  /**
   * Stop the server from accepting new connections.
   * @param closeActiveConnections If true, immediately terminates all connections
   * @returns Promise that resolves when the server has stopped
   */
  stop(closeActiveConnections?: boolean): Promise<void>;

  /**
   * Update handlers without restarting the server.
   * Only fetch and error handlers can be updated.
   */
  reload(options: Serve): void;

  /**
   * Make a request to the running server.
   * Useful for testing or internal routing.
   */
  fetch(request: Request | string): Response | Promise<Response>;

  /**
   * Upgrade an HTTP request to a WebSocket connection.
   * @returns true if upgrade successful, false if failed
   */
  upgrade<T = undefined>(
    request: Request,
    options?: {
      headers?: Bun.HeadersInit;
      data?: T;
    },
  ): boolean;

  /**
   * Publish a message to all WebSocket clients subscribed to a topic.
   * @returns Bytes sent, 0 if dropped, -1 if backpressure applied
   */
  publish(
    topic: string,
    data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
    compress?: boolean,
  ): ServerWebSocketSendStatus;

  /**
   * Get count of WebSocket clients subscribed to a topic.
   */
  subscriberCount(topic: string): number;

  /**
   * Get client IP address and port.
   * @returns null for closed requests or Unix sockets
   */
  requestIP(request: Request): SocketAddress | null;

  /**
   * Set custom idle timeout for a request.
   * @param seconds Timeout in seconds, 0 to disable
   */
  timeout(request: Request, seconds: number): void;

  /**
   * Keep process alive while server is running.
   */
  ref(): void;

  /**
   * Allow process to exit if server is only thing running.
   */
  unref(): void;

  /** Number of in-flight HTTP requests */
  readonly pendingRequests: number;

  /** Number of active WebSocket connections */
  readonly pendingWebSockets: number;

  /** Server URL including protocol, hostname and port */
  readonly url: URL;

  /** Port server is listening on */
  readonly port: number;

  /** Hostname server is bound to */
  readonly hostname: string;

  /** Whether server is in development mode */
  readonly development: boolean;

  /** Server instance identifier */
  readonly id: string;
}

interface WebSocketHandler<T = undefined> {
  /** Maximum WebSocket message size in bytes */
  maxPayloadLength?: number;

  /** Bytes of queued messages before applying backpressure */
  backpressureLimit?: number;

  /** Whether to close connection when backpressure limit hit */
  closeOnBackpressureLimit?: boolean;

  /** Called when backpressure is relieved */
  drain?(ws: ServerWebSocket<T>): void | Promise<void>;

  /** Seconds before idle timeout */
  idleTimeout?: number;

  /** Enable per-message deflate compression */
  perMessageDeflate?:
    | boolean
    | {
        compress?: WebSocketCompressor | boolean;
        decompress?: WebSocketCompressor | boolean;
      };

  /** Send ping frames to keep connection alive */
  sendPings?: boolean;

  /** Whether server receives its own published messages */
  publishToSelf?: boolean;

  /** Called when connection opened */
  open?(ws: ServerWebSocket<T>): void | Promise<void>;

  /** Called when message received */
  message(ws: ServerWebSocket<T>, message: string | Buffer): void | Promise<void>;

  /** Called when connection closed */
  close?(ws: ServerWebSocket<T>, code: number, reason: string): void | Promise<void>;

  /** Called when ping frame received */
  ping?(ws: ServerWebSocket<T>, data: Buffer): void | Promise<void>;

  /** Called when pong frame received */
  pong?(ws: ServerWebSocket<T>, data: Buffer): void | Promise<void>;
}

interface TLSOptions {
  /** Certificate authority chain */
  ca?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

  /** Server certificate */
  cert?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

  /** Path to DH parameters file */
  dhParamsFile?: string;

  /** Private key */
  key?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;

  /** Reduce TLS memory usage */
  lowMemoryMode?: boolean;

  /** Private key passphrase */
  passphrase?: string;

  /** OpenSSL options flags */
  secureOptions?: number;

  /** Server name for SNI */
  serverName?: string;
}