Skip to main content
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')
  }
}
code
number
default:"1000"
Close code (1000 = normal closure)
reason
string
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:
1000
Normal Closure
Successful operation / regular socket shutdown
1001
Going Away
Browser tab closed or server is shutting down
1002
Protocol Error
Endpoint received a malformed frame
1003
Unsupported Data
Endpoint received data it cannot accept
1006
Abnormal Closure
No close frame received (connection lost)
1007
Invalid Data
Endpoint received inconsistent message
1008
Policy Violation
Endpoint received message violating policy
1009
Message Too Big
Message is too big to process
1011
Internal Error
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

FeatureWebSocketSSE
DirectionBidirectionalServer to client only
ProtocolWebSocket (ws://)HTTP
Browser SupportModern browsersAll modern browsers
ComplexityHigherLower
Auto ReconnectManualAutomatic
Binary SupportYesNo
Choose WebSocket when you need bidirectional communication. Use SSE for simpler server-to-client streaming.

Build docs developers (and LLMs) love