WebSockets
Server-side WebSockets in Bun
Bun.serve() supports server-side WebSockets, with on-the-fly compression, TLS support, and a Bun-native publish-subscribe API.
⚡️ 7x more throughput
Bun's WebSockets are fast. For a simple chatroom on Linux x64, Bun can handle 7x more messages per second than Node.js + "ws".
| Messages sent per second | Runtime | Clients |
|---|---|---|
| ~700,000 | (Bun.serve) Bun v0.2.1 (x64) | 16 |
| ~100,000 | (ws) Node v18.10.0 (x64) | 16 |
Internally Bun's WebSocket implementation is built on uWebSockets.
Start a WebSocket server#
The following server, built with Bun.serve, upgrades every incoming request to a WebSocket connection in the fetch handler. You declare the socket handlers in the websocket parameter.
Bun.serve({
fetch(req, server) {
// upgrade the request to a WebSocket
if (server.upgrade(req)) {
return; // do not return a Response
}
return new Response("Upgrade failed", { status: 500 });
},
websocket: {}, // handlers
});Bun supports these WebSocket event handlers:
Bun.serve({
fetch(req, server) {}, // upgrade logic
websocket: {
message(ws, message) {}, // a message is received
open(ws) {}, // a socket is opened
close(ws, code, message) {}, // a socket is closed
drain(ws) {}, // the socket is ready to receive more data
},
});An API designed for speed
In Bun, you declare handlers once per server, instead of per socket.
You pass a single WebSocketHandler object to Bun.serve() with methods for open, message, close, drain, and error. This design differs from the client-side WebSocket class, which extends EventTarget (onmessage, onopen, onclose).
Clients tend to have few socket connections open, so an event-based API makes sense there.
But servers tend to have many socket connections open, which means:
- Time spent adding/removing event listeners for each connection adds up
- Extra memory spent on storing references to callback functions for each connection
- Usually, people create new functions for each connection, which also means more memory
Reusing one handler object across every connection avoids both costs.
The first argument to each handler is the ServerWebSocket instance handling the event. The ServerWebSocket class is a fast, Bun-native implementation of WebSocket with some additional features.
Bun.serve({
fetch(req, server) {}, // upgrade logic
websocket: {
message(ws, message) {
ws.send(message); // echo back the message
},
},
});Sending messages#
Each ServerWebSocket instance has a .send() method for sending messages to the client. It supports a range of input types.
Bun.serve({
fetch(req, server) {}, // upgrade logic
websocket: {
message(ws, message) {
ws.send("Hello world"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
ws.send(new Blob(["binary"])); // Blob
},
},
});Headers#
Once the upgrade succeeds, Bun sends a 101 Switching Protocols response per the spec. To attach additional headers to this Response, pass them to server.upgrade().
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // handlers
});Contextual data#
Attach contextual data to a new WebSocket in the .upgrade() call. It is available on the ws.data property inside the WebSocket handlers.
To strongly type ws.data, add a data property to the websocket handler object. This types ws.data across all lifecycle hooks.
type WebSocketData = {
createdAt: number;
channelId: string;
authToken: string;
};
Bun.serve({
fetch(req, server) {
const cookies = new Bun.CookieMap(req.headers.get("cookie")!);
server.upgrade(req, {
// this object must conform to WebSocketData
data: {
createdAt: Date.now(),
channelId: new URL(req.url).searchParams.get("channelId"),
authToken: cookies.get("X-Token"),
},
});
return undefined;
},
websocket: {
// TypeScript: specify the type of ws.data like this
data: {} as WebSocketData,
// handler called when a message is received
async message(ws, message) {
// ws.data is now properly typed as WebSocketData
const user = getUserFromToken(ws.data.authToken);
await saveMessageToDatabase({
channel: ws.data.channelId,
message: String(message),
userId: user.id,
});
},
},
});Previously, you could specify the type of ws.data with a type parameter on Bun.serve, like Bun.serve<MyData>({...}). Bun removed this pattern in favor of the data property because of a limitation in TypeScript.
To connect to this server from the browser, create a new WebSocket.
const socket = new WebSocket("ws://localhost:3000/chat");
socket.addEventListener("message", event => {
console.log(event.data);
});Identifying users
The browser sends cookies set on the page along with the WebSocket upgrade request. They are available on req.headers in the fetch handler. Parse them to identify the connecting user and set data accordingly.
Pub/Sub#
Bun's ServerWebSocket includes a native publish-subscribe API for topic-based broadcasting. You specify a topic with a string identifier. An individual socket can .subscribe() to a topic and .publish() messages to all other subscribers to that topic (excluding itself). This topic-based broadcast API is similar to MQTT and Redis Pub/Sub.
const server = Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/chat") {
console.log(`upgrade!`);
const username = getUsernameFromReq(req);
const success = server.upgrade(req, { data: { username } });
return success ? undefined : new Response("WebSocket upgrade error", { status: 400 });
}
return new Response("Hello world");
},
websocket: {
// TypeScript: specify the type of ws.data like this
data: {} as { username: string },
open(ws) {
const msg = `${ws.data.username} has entered the chat`;
ws.subscribe("the-group-chat");
server.publish("the-group-chat", msg);
},