This document explains the proper way to implement WebSocket with Hono and Bun based on official documentation and best practices.
Hono provides createBunWebSocket() specifically for Bun runtime. This function returns two important items:
import { createBunWebSocket } from 'hono/bun';
const { upgradeWebSocket, websocket } = createBunWebSocket();upgradeWebSocket: A function to create WebSocket route handlerswebsocket: A WebSocket handler object forBun.serve()
The correct pattern for setting up a Bun server with Hono and WebSocket:
import { Hono } from 'hono';
import { createBunWebSocket } from 'hono/bun';
const app = new Hono();
const { upgradeWebSocket, websocket } = createBunWebSocket();
// Define WebSocket routes
app.get('/ws', upgradeWebSocket((c) => {
return {
onOpen(event, ws) {
console.log('Connection opened');
},
onMessage(event, ws) {
console.log('Message:', event.data);
ws.send('Response from server');
},
onClose(event, ws) {
console.log('Connection closed');
},
onError(event, ws) {
console.error('Error:', event);
}
};
}));
// Start server with websocket handler
const server = Bun.serve({
port: 3000,
fetch: app.fetch,
websocket, // CRITICAL: Must include this
});- Always use
createBunWebSocket()for Bun runtime - Pass
websockettoBun.serve()- This is required for WebSocket to work - Use
event.datato access message content inonMessage - Use
ws.send()to send messages back to client - Handle errors in try-catch blocks
- Don't use
upgradeWebSocketfromhono/bundirectly - usecreateBunWebSocket()instead - Don't forget the
websocketinBun.serve()- WebSocket won't work without it - Don't define custom websocket handlers in
Bun.serve()- let Hono handle it
Called when a WebSocket connection is established.
onOpen(event, ws) {
console.log('Client connected');
ws.send(JSON.stringify({ type: 'welcome' }));
}Called when a message is received from the client.
onMessage(event, ws) {
const message = event.data; // String or Buffer
console.log('Received:', message);
// Parse JSON if needed
try {
const data = JSON.parse(message);
// Handle data
} catch (error) {
console.error('Invalid JSON');
}
}Called when the connection is closed.
onClose(event, ws) {
console.log('Client disconnected');
// Cleanup logic here
}Called when an error occurs.
onError(event, ws) {
console.error('WebSocket error:', event);
}For Bun-specific features like pub/sub, access the raw WebSocket:
import type { ServerWebSocket } from 'bun';
onOpen(event, ws) {
const rawWs = ws.raw as ServerWebSocket;
rawWs.subscribe('my-topic');
}
onClose(event, ws) {
const rawWs = ws.raw as ServerWebSocket;
rawWs.unsubscribe('my-topic');
}Then use server.publish() to broadcast:
server.publish('my-topic', JSON.stringify({ message: 'Hello all!' }));apps/backend/src/
├── index.ts # Main server setup
├── config.ts # Configuration
├── http/
│ └── server.ts # HTTP routes
└── ws/
├── server.ts # WebSocket setup
├── connection.ts # Connection management
├── messageHandler.ts # Message routing
├── broadcast.ts # Broadcasting logic
└── polaroidQueue.ts # Queue management
import { createBunWebSocket } from 'hono/bun';
const { upgradeWebSocket, websocket } = createBunWebSocket();
const activeConnections = new Set();
export function createWebSocketServer(app: Hono) {
app.get('/ws', upgradeWebSocket((c) => {
return {
onOpen(event, ws) {
activeConnections.add(ws);
ws.send(JSON.stringify({ type: 'connected' }));
},
onMessage(event, ws) {
const buffer = Buffer.from(event.data);
handleMessage(ws, { clients: activeConnections }, buffer);
},
onClose(event, ws) {
activeConnections.delete(ws);
}
};
}));
return { clients: activeConnections, websocket };
}
export { websocket };import { createWebSocketServer, websocket } from './ws/server';
createWebSocketServer(httpServer);
const server = Bun.serve({
port: PORT,
fetch: httpServer.fetch,
websocket, // Required!
});- Client connects →
onOpentriggered - Client sends message →
onMessagereceivesevent.data - Server processes →
handleMessageparses and routes - Server responds →
ws.send()sends back to client - Client disconnects →
onClosetriggered
onMessage(event, ws) {
console.log('✅ onMessage triggered!');
console.log('Data:', event.data);
}const server = Bun.serve({
port: PORT,
fetch: app.fetch,
websocket, // Make sure this is here!
});const ws = new WebSocket('ws://localhost:3001/ws');
ws.onopen = () => console.log('Connected');
ws.onmessage = (e) => console.log('Message:', e.data);
ws.send(JSON.stringify({ type: 'test' }));Solution: Add websocket to Bun.serve() configuration
Solution:
- Verify
websocketis inBun.serve() - Check if
createBunWebSocket()is used correctly - Ensure route is
/wsand client connects to correct URL
Solution: Check event.data format and parsing logic
- Hono WebSocket Helper Docs
- Bun WebSocket API
- DEV Community Tutorial
- Hono GitHub - Bun WebSocket Implementation
# Terminal 1: Start backend
cd apps/backend
bun run dev
# Terminal 2: Start frontend
cd apps/tablet
bun run dev
# Open browser: http://localhost:5173
# Check console for WebSocket logs- Backend:
wss://orbit-5awh.onrender.com/ws - Frontend:
https://orbit-robo.vercel.app
The key to Hono + Bun WebSocket is:
- Use
createBunWebSocket()to get bothupgradeWebSocketandwebsocket - Pass
websockettoBun.serve() - Handle events in
upgradeWebSocket()callback - Use
event.datafor messages andws.send()for responses
This pattern ensures proper WebSocket functionality in production and development.