The WebSocket helper provides a runtime-agnostic way to handle WebSocket connections in Hono applications.
Overview
Hono’s WebSocket helper is a wrapper that works across different runtimes (Cloudflare Workers, Deno, Bun, etc.) by abstracting WebSocket implementation details.
The WebSocket helper itself doesn’t implement WebSockets. You need to use runtime-specific adaptors that implement the upgradeWebSocket function.
Adaptors
Use runtime-specific adaptors:
Cloudflare Workers : hono/cloudflare-workers
Deno : hono/deno
Bun : hono/bun
Node.js : Third-party adaptor needed
Import
import { upgradeWebSocket } from 'hono/cloudflare-workers'
// or 'hono/deno' or 'hono/bun' depending on runtime
Basic Usage
Simple WebSocket Server
import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/cloudflare-workers'
const app = new Hono ()
app . get (
'/ws' ,
upgradeWebSocket (( c ) => {
return {
onMessage ( event , ws ) {
console . log ( `Message from client: ${ event . data } ` )
ws . send ( 'Hello from server!' )
},
onClose () {
console . log ( 'Connection closed' )
},
}
})
)
export default app
Client-Side Connection
const ws = new WebSocket ( 'ws://localhost:8787/ws' )
ws . onopen = () => {
console . log ( 'Connected' )
ws . send ( 'Hello Server!' )
}
ws . onmessage = ( event ) => {
console . log ( 'Message from server:' , event . data )
}
ws . onclose = () => {
console . log ( 'Disconnected' )
}
ws . onerror = ( error ) => {
console . error ( 'WebSocket error:' , error )
}
Event Handlers
The WebSocket helper provides four event handlers:
onOpen
Called when the connection is established:
app . get (
'/ws' ,
upgradeWebSocket (( c ) => {
return {
onOpen ( event , ws ) {
console . log ( 'Client connected' )
console . log ( 'Connection URL:' , ws . url ?. href )
console . log ( 'Protocol:' , ws . protocol )
},
}
})
)
onMessage
Called when a message is received:
app . get (
'/ws' ,
upgradeWebSocket (( c ) => {
return {
onMessage ( event , ws ) {
const message = event . data
if ( typeof message === 'string' ) {
ws . send ( `Echo: ${ message } ` )
} else if ( message instanceof Blob ) {
console . log ( 'Received blob' )
} else if ( message instanceof ArrayBuffer ) {
console . log ( 'Received binary data' )
}
},
}
})
)
onClose
Called when the connection closes:
app . get (
'/ws' ,
upgradeWebSocket (( c ) => {
return {
onClose ( event , ws ) {
console . log ( 'Connection closed' )
console . log ( 'Code:' , event . code )
console . log ( 'Reason:' , event . reason )
},
}
})
)
onError
Called when an error occurs:
app . get (
'/ws' ,
upgradeWebSocket (( c ) => {
return {
onError ( event , ws ) {
console . error ( 'WebSocket error occurred' )
},
}
})
)
WSContext
The ws parameter in event handlers is a WSContext instance:
send()
Send data to the client:
onMessage ( event , ws ) {
// Send string
ws . send ( 'Hello' )
// Send binary data
ws . send ( new Uint8Array ([ 1 , 2 , 3 ]))
// Send with options
ws . send ( 'compressed message' , { compress: true })
}
close()
Close the connection:
onMessage ( event , ws ) {
if ( event . data === 'bye' ) {
ws . close ( 1000 , 'Goodbye' )
}
}
Close code (1000 = normal closure)
Human-readable close reason
readyState
Get the connection state:
onMessage ( event , ws ) {
console . log ( 'Ready state:' , ws . readyState )
// 0 = CONNECTING
// 1 = OPEN
// 2 = CLOSING
// 3 = CLOSED
if ( ws . readyState === 1 ) {
ws . send ( 'Connection is open' )
}
}
url
Access the connection URL:
onOpen ( event , ws ) {
if ( ws . url ) {
console . log ( 'Full URL:' , ws . url . href )
console . log ( 'Path:' , ws . url . pathname )
console . log ( 'Query:' , ws . url . searchParams )
}
}
protocol
Get the negotiated subprotocol:
onOpen ( event , ws ) {
console . log ( 'Protocol:' , ws . protocol )
}
raw
Access the underlying WebSocket:
onOpen ( event , ws ) {
// Runtime-specific WebSocket instance
console . log ( 'Raw WebSocket:' , ws . raw )
}
Advanced Usage
Chat Server Example
import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/cloudflare-workers'
const app = new Hono ()
// Store active connections
const connections = new Set < WSContext >()
app . get (
'/chat' ,
upgradeWebSocket (( c ) => {
let username = 'Anonymous'
return {
onOpen ( event , ws ) {
connections . add ( ws )
console . log ( `User connected. Total: ${ connections . size } ` )
},
onMessage ( event , ws ) {
const message = event . data
if ( typeof message === 'string' ) {
// Parse message
try {
const data = JSON . parse ( message )
if ( data . type === 'setName' ) {
username = data . name
} else if ( data . type === 'message' ) {
// Broadcast to all clients
const broadcast = JSON . stringify ({
username ,
message: data . message ,
timestamp: Date . now (),
})
for ( const conn of connections ) {
if ( conn . readyState === 1 ) {
conn . send ( broadcast )
}
}
}
} catch ( e ) {
ws . send ( JSON . stringify ({ error: 'Invalid message format' }))
}
}
},
onClose ( event , ws ) {
connections . delete ( ws )
console . log ( `User disconnected. Total: ${ connections . size } ` )
},
onError ( event , ws ) {
console . error ( 'WebSocket error' )
connections . delete ( ws )
},
}
})
)
Using Context Data
Access Hono context inside WebSocket handlers:
app . get (
'/ws' ,
upgradeWebSocket (( c ) => {
const userId = c . req . query ( 'userId' )
const token = c . req . header ( 'Authorization' )
return {
onOpen ( event , ws ) {
console . log ( `User ${ userId } connected with token ${ token } ` )
},
onMessage ( event , ws ) {
// userId and token are accessible via closure
ws . send ( `Hello user ${ userId } ` )
},
}
})
)
Binary Data
Handle binary messages:
app . get (
'/binary' ,
upgradeWebSocket (( c ) => {
return {
onMessage ( event , ws ) {
const data = event . data
if ( data instanceof ArrayBuffer ) {
const view = new Uint8Array ( data )
console . log ( 'Received bytes:' , view . length )
// Echo back
ws . send ( data )
}
},
}
})
)
Direct Upgrade
Upgrade in a handler directly:
import { upgradeWebSocket } from 'hono/cloudflare-workers'
app . get ( '/ws' , async ( c ) => {
// Check authentication
const token = c . req . header ( 'Authorization' )
if ( ! token ) {
return c . text ( 'Unauthorized' , 401 )
}
// Upgrade to WebSocket
return upgradeWebSocket ( c , {
onMessage ( event , ws ) {
ws . send ( 'Authenticated message' )
},
})
})
Testing WebSockets
Test WebSocket endpoints:
import { testClient } from 'hono/testing'
const client = testClient ( app )
// Note: WebSocket testing requires special handling
// Use runtime-specific testing tools
Creating Custom Adaptors
Create a custom WebSocket adaptor:
import { defineWebSocketHelper } from 'hono/websocket'
import type { WSEvents } from 'hono/websocket'
export const upgradeWebSocket = defineWebSocketHelper (
( c , events , options ) => {
// Implement runtime-specific WebSocket upgrade logic
// Return Response or void
}
)
Close Codes
Standard WebSocket close codes:
Successful operation / regular socket shutdown
Browser tab closed or server is shutting down
Endpoint received a malformed frame
Endpoint received data it cannot accept
No close frame received (connection lost)
Endpoint received inconsistent message
Endpoint received message violating policy
Message is too big to process
Server encountered an unexpected condition
Use Cases
Real-time Chat Build chat applications with instant message delivery
Live Updates Push live data updates to connected clients
Gaming Real-time multiplayer game state synchronization
Collaboration Collaborative editing and presence features
Best Practices
Always handle connection cleanup in onClose and onError to prevent memory leaks.
WebSocket connections are stateful and consume resources. Implement connection limits and timeouts for production.
For one-way server-to-client communication, consider using Server-Sent Events (SSE) instead, which is simpler and works over HTTP.
Comparison with SSE
Feature WebSocket SSE Direction Bidirectional Server to client only Protocol WebSocket (ws://) HTTP Browser Support Modern browsers All modern browsers Complexity Higher Lower Auto Reconnect Manual Automatic Binary Support Yes No
Choose WebSocket when you need bidirectional communication. Use SSE for simpler server-to-client streaming.