Bun.serve()
supports server-side WebSockets, with on-the-fly compression, TLS support, and a Bun-native publish-subscribe API.
{% callout %}
⚡️ 7x more throughput — Bun's WebSockets are fast. For a simple chatroom on Linux x64, Bun can handle 7x more requests 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. {% /callout %}
To connect to an external socket server, create an instance of WebSocket
with the constructor.
const socket = new WebSocket("ws://localhost:3000");
Bun supports setting custom headers. This is a Bun-specific extension of the WebSocket
standard.
const socket = new WebSocket("ws://localhost:3000", {
headers: {
// custom headers
},
});
To add event listeners to the socket:
// message is received
socket.addEventListener("message", event => {});
// socket opened
socket.addEventListener("open", event => {});
// socket closed
socket.addEventListener("close", event => {});
// error handler
socket.addEventListener("error", event => {});
Below is a simple WebSocket server built with Bun.serve
, in which all incoming requests are upgraded to WebSocket connections in the fetch
handler. The socket handlers are declared 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
});
The following WebSocket event handlers are supported:
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
},
});
{% details summary="An API designed for speed" %}
In Bun, handlers are declared once per server, instead of per socket.
ServerWebSocket
expects you to pass a WebSocketHandler
object to the Bun.serve()
method which has methods for open
, message
, close
, drain
, and error
. This is different than the client-side WebSocket
class which extends EventTarget
(onmessage, onopen, onclose),
Clients tend to not have many socket connections open so an event-based API makes sense.
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 callbacks function for each connection
- Usually, people create new functions for each connection, which also means more memory
So, instead of using an event-based API, ServerWebSocket
expects you to pass a single object with methods for each event in Bun.serve()
and it is reused for each connection.
This leads to less memory usage and less time spent adding/removing event listeners. {% /details %}
The first argument to each handler is the instance of ServerWebSocket
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
},
},
});
Each ServerWebSocket
instance has a .send()
method for sending messages to the client. It supports a range of input types.
ws.send("Hello world"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
Once the upgrade succeeds, Bun will send a 101 Switching Protocols
response per the spec. Additional headers
can be attched to this Response
in the call to server.upgrade()
.
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // handlers
});
Contextual data
can be attached to a new WebSocket in the .upgrade()
call. This data is made available on the ws.data
property inside the WebSocket handlers.
type WebSocketData = {
createdAt: number;
channelId: string;
};
// TypeScript: specify the type of `data`
Bun.serve<WebSocketData>({
fetch(req, server) {
server.upgrade(req, {
// TS: this object must conform to WebSocketData
data: {
createdAt: Date.now(),
channelId: new URL(req.url).searchParams.get("channelId"),
},
});
return undefined;
},
websocket: {
// handler called when a message is received
async message(ws, message) {
ws.data; // WebSocketData
await saveMessageToDatabase({
channel: ws.data.channelId,
message: String(message),
});
},
},
});
Bun's ServerWebSocket
implementation implements a native publish-subscribe API for topic-based broadcasting. Individual sockets can .subscribe()
to a topic (specified with a string identifier) and .publish()
messages to all other subscribers to that topic. This topic-based broadcast API is similar to MQTT and Redis Pub/Sub.
const pubsubserver = Bun.serve<{username: string}>({
fetch(req, server) {
if (req.url === '/chat') {
const cookies = getCookieFromRequest(req);
const success = server.upgrade(req, {
data: {username: cookies.username},
});
return success
? undefined
: new Response('WebSocket upgrade error', {status: 400});
}
return new Response('Hello world');
},
websocket: {
open(ws) {
ws.subscribe('the-group-chat');
ws.publish('the-group-chat', `${ws.data.username} has entered the chat`);
},
message(ws, message) {
// this is a group chat
// so the server re-broadcasts incoming message to everyone
ws.publish('the-group-chat', `${ws.data.username}: ${message}`);
},
close(ws) {
ws.unsubscribe('the-group-chat');
ws.publish('the-group-chat', `${ws.data.username} has left the chat`);
},
});
Per-message compression can be enabled with the perMessageDeflate
parameter.
Bun.serve({
fetch(req, server) {}, // upgrade logic
websocket: {
// enable compression and decompression
perMessageDeflate: true,
},
});
Compression can be enabled for individual messages by passing a boolean
as the second argument to .send()
.
ws.send("Hello world", true);
For fine-grained control over compression characteristics, refer to the Reference.
The .send(message)
method of ServerWebSocket
returns a number
indicating the result of the operation.
-1
— The message was enqueued but there is backpressure0
— The message was dropped due to a connection issue1+
— The number of bytes sent
This gives you better control over backpressure in your server.
namespace Bun {
export function serve(params: {
fetch: (req: Request, server: Server) => Response | Promise<Response>;
websocket?: {
message: (ws: ServerWebSocket, message: string | ArrayBuffer | Uint8Array) => void;
open?: (ws: ServerWebSocket) => void;
close?: (ws: ServerWebSocket) => void;
error?: (ws: ServerWebSocket, error: Error) => void;
drain?: (ws: ServerWebSocket) => void;
perMessageDeflate?:
| boolean
| {
compress?: boolean | Compressor;
decompress?: boolean | Compressor;
};
};
}): Server;
}
type Compressor =
| `"disable"`
| `"shared"`
| `"dedicated"`
| `"3KB"`
| `"4KB"`
| `"8KB"`
| `"16KB"`
| `"32KB"`
| `"64KB"`
| `"128KB"`
| `"256KB"`;
interface Server {
pendingWebsockets: number;
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number;
upgrade(
req: Request,
options?: {
headers?: HeadersInit;
data?: any;
},
): boolean;
}
interface ServerWebSocket {
readonly data: any;
readonly readyState: number;
readonly remoteAddress: string;
send(message: string | ArrayBuffer | Uint8Array, compress?: boolean): number;
close(code?: number, reason?: string): void;
subscribe(topic: string): void;
unsubscribe(topic: string): void;
publish(topic: string, message: string | ArrayBuffer | Uint8Array): void;
isSubscribed(topic: string): boolean;
cork(cb: (ws: ServerWebSocket) => void): void;
}