Skip to main content
Bun provides low-level TCP and UDP APIs for performance-sensitive networking where HTTP overhead is undesirable — think database clients, game servers, or custom protocols.
These are low-level APIs intended for library authors and advanced use cases. For most applications, prefer Bun.serve() (HTTP) or fetch().

TCP server with Bun.listen()

Bun.listen({
  hostname: "localhost",
  port: 8080,
  socket: {
    open(socket) {
      console.log("Client connected");
      socket.write("Welcome!\n");
    },
    data(socket, data) {
      console.log("Received:", data.toString());
    },
    close(socket) {
      console.log("Client disconnected");
    },
    drain(socket) {
      // Called when the socket is ready to receive more data after backpressure
    },
    error(socket, error) {
      console.error("Socket error:", error);
    },
  },
});
All event handlers are declared once per server and shared across all connections — this avoids garbage-collector pressure from per-socket closures and keeps memory usage flat.

Attaching data to a socket

Attach contextual data to each socket in the open handler. Access it via socket.data in any handler.
type SocketData = {
  sessionId: string;
  connectedAt: number;
};

Bun.listen<SocketData>({
  hostname: "localhost",
  port: 8080,
  socket: {
    open(socket) {
      socket.data = {
        sessionId: crypto.randomUUID(),
        connectedAt: Date.now(),
      };
    },
    data(socket, data) {
      socket.write(`[${socket.data.sessionId}] ack: ${data}`);
    },
  },
});

Stopping a TCP server

const server = Bun.listen({
  hostname: "localhost",
  port: 8080,
  socket: {
    data(socket, data) { socket.write(data); },
  },
});

// Stop accepting new connections; close active ones immediately
server.stop(true);

// Allow process to exit even while server is still listening
server.unref();

TCP echo server (complete example)

1

Create the server

server.ts
const server = Bun.listen({
  hostname: "localhost",
  port: 9000,
  socket: {
    open(socket) {
      console.log(`[server] client connected: ${socket.remoteAddress}`);
    },
    data(socket, data) {
      console.log(`[server] received: ${data}`);
      socket.write(data); // echo back
    },
    close(socket) {
      console.log("[server] client disconnected");
    },
    error(socket, error) {
      console.error("[server] error:", error.message);
    },
  },
});

console.log(`TCP echo server listening on port ${server.port}`);
2

Create the client

client.ts
const socket = await Bun.connect({
  hostname: "localhost",
  port: 9000,
  socket: {
    open(socket) {
      socket.write("Hello, TCP!");
    },
    data(socket, data) {
      console.log(`[client] echo: ${data}`);
      socket.end();
    },
    close(socket) {
      console.log("[client] connection closed");
    },
    error(socket, error) {
      console.error("[client] error:", error.message);
    },
  },
});

TCP client with Bun.connect()

Bun.connect() supports the same socket handlers as Bun.listen() plus three client-specific ones:
const socket = await Bun.connect({
  hostname: "database.internal",
  port: 5432,
  socket: {
    open(socket) {
      console.log("Connected to DB");
    },
    data(socket, data) {
      console.log("Response:", data.toString("hex"));
    },
    close(socket) {},
    drain(socket) {},
    error(socket, error) {},

    // Client-only handlers
    connectError(socket, error) {
      console.error("Connection failed:", error.message);
    },
    end(socket) {
      console.log("Server closed the connection");
    },
    timeout(socket) {
      console.log("Connection timed out");
    },
  },
});

// Send data
socket.write(Buffer.from([0x00, 0x01, 0x02]));

TLS over TCP

TLS server

Bun.listen({
  hostname: "localhost",
  port: 8443,
  socket: {
    data(socket, data) {
      socket.write(data); // echo
    },
  },
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
});
key and cert accept a string, BunFile, Buffer, TypedArray, or an array of any of those.

TLS client

const socket = await Bun.connect({
  hostname: "secure.example.com",
  port: 443,
  tls: true, // use TLS with default settings
  socket: {
    data(socket, data) {
      console.log(data.toString());
    },
  },
});

Hot reloading

Both servers and individual sockets support hot-swapping their handlers at runtime without dropping connections:
const server = Bun.listen({
  hostname: "localhost",
  port: 8080,
  socket: {
    data(socket, data) {
      socket.write("v1 response");
    },
  },
});

server.reload({
  socket: {
    data(socket, data) {
      socket.write("v2 response");
    },
  },
});

Buffering TCP writes

TCP sockets in Bun do not automatically buffer writes. Many small socket.write() calls perform significantly worse than a single larger one. Use ArrayBufferSink to accumulate data before flushing:
import { ArrayBufferSink } from "bun";

const sink = new ArrayBufferSink();
sink.start({
  stream: true,
  highWaterMark: 1024,
});

sink.write("h");
sink.write("e");
sink.write("l");
sink.write("l");
sink.write("o");

queueMicrotask(() => {
  const data = sink.flush();
  const wrote = socket.write(data);

  if (wrote < data.byteLength) {
    // Re-queue the unsent bytes; drain() will be called when ready
    sink.write(data.subarray(wrote));
  }
});

UDP with Bun.udpSocket()

UDP is connectionless and lower latency than TCP. It is suited for real-time applications like voice chat, gaming, or DNS clients where occasional packet loss is acceptable.

Create a UDP socket

// Port assigned by the OS
const socket = await Bun.udpSocket({});
console.log(socket.port);

// Specific port
const socket2 = await Bun.udpSocket({ port: 41234 });

Send a datagram

// send(data, port, address)
socket.send("Hello, world!", 41234, "127.0.0.1");
send() requires a resolved IP address. It does not perform DNS lookups in order to keep latency as low as possible.

Receive datagrams

const server = await Bun.udpSocket({
  socket: {
    data(socket, buf, port, addr) {
      console.log(`Message from ${addr}:${port}: ${buf.toString()}`);
      socket.send("Pong!", port, addr);
    },
  },
});

const client = await Bun.udpSocket({});
client.send("Ping!", server.port, "127.0.0.1");

Connected UDP socket

Connecting a UDP socket binds it to a single peer — all sends go to that peer and only packets from that peer are received. This can also improve performance on some operating systems.
const server = await Bun.udpSocket({
  socket: {
    data(socket, buf, port, addr) {
      console.log(`${addr}:${port}: ${buf}`);
    },
  },
});

const client = await Bun.udpSocket({
  connect: {
    port: server.port,
    hostname: "127.0.0.1",
  },
});

// No need to specify destination — it was set in `connect`
client.send("Hello from connected socket");

Batch sends with sendMany()

Amortize the system-call cost when sending many datagrams at once:
const socket = await Bun.udpSocket({});

// Each group of three elements: [data, port, address]
socket.sendMany([
  "Hello", 41234, "127.0.0.1",
  "World", 41235, "127.0.0.1",
]);
sendMany() returns the number of packets successfully sent.

Backpressure

When the OS packet buffer is full, send() returns false and sendMany() returns fewer packets than requested. The drain handler fires when the socket is writable again:
const socket = await Bun.udpSocket({
  socket: {
    drain(socket) {
      // Resume sending
      socket.send("Resumed!", 41234, "127.0.0.1");
    },
  },
});

Socket options

const socket = await Bun.udpSocket({});

// Allow sending to broadcast addresses
socket.setBroadcast(true);

// Set IP time-to-live for outgoing packets
socket.setTTL(64);

Multicast

const socket = await Bun.udpSocket({});

// Join a multicast group
socket.addMembership("224.0.0.1");

// Join on a specific interface
socket.addMembership("224.0.0.1", "192.168.1.100");

// Set TTL for multicast packets (network hops)
socket.setMulticastTTL(4);

// Control loopback of multicast packets to local socket
socket.setMulticastLoopback(true);

// Specify outgoing multicast interface
socket.setMulticastInterface("192.168.1.100");

// Leave a group
socket.dropMembership("224.0.0.1");
Source-specific multicast (SSM):
socket.addSourceSpecificMembership("10.0.0.1", "232.0.0.1");
socket.dropSourceSpecificMembership("10.0.0.1", "232.0.0.1");

Build docs developers (and LLMs) love