Low-level TCP and UDP networking with Bun.listen(), Bun.connect(), and Bun.udpSocket() for building database clients, game servers, and real-time applications.
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().
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.
const server = Bun.listen({ hostname: "localhost", port: 8080, socket: { data(socket, data) { socket.write(data); }, },});// Stop accepting new connections; close active ones immediatelyserver.stop(true);// Allow process to exit even while server is still listeningserver.unref();
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 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.
// Port assigned by the OSconst socket = await Bun.udpSocket({});console.log(socket.port);// Specific portconst socket2 = await Bun.udpSocket({ port: 41234 });
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");
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.
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({});// Allow sending to broadcast addressessocket.setBroadcast(true);// Set IP time-to-live for outgoing packetssocket.setTTL(64);
const socket = await Bun.udpSocket({});// Join a multicast groupsocket.addMembership("224.0.0.1");// Join on a specific interfacesocket.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 socketsocket.setMulticastLoopback(true);// Specify outgoing multicast interfacesocket.setMulticastInterface("192.168.1.100");// Leave a groupsocket.dropMembership("224.0.0.1");