WebSocket
Accept server-side WebSocket connections in a Go worker.
The exp/cloudflare/websocket package implements the server side of the Workers WebSocket API: it creates a WebSocketPair, accepts the server end, and attaches the client end to the HTTP response.
Upgrade a request
websocket.Upgrade(w, r) checks the Upgrade: websocket header and returns a *websocket.Conn. The 101 response is only sent once the handler returns, so the read/write loop must run in a goroutine and the handler must return immediately without touching the connection.
func handleWebSocket(w http.ResponseWriter, req *http.Request) {
conn, err := websocket.Upgrade(w, req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
go echo(conn) // return immediately — the goroutine keeps the worker alive
}
func echo(conn *websocket.Conn) {
defer conn.Close(0, "")
for {
mt, data, err := conn.ReadMessage(context.Background())
if err != nil {
return // websocket.ErrClosed once the peer closes
}
if err := conn.WriteMessage(mt, data); err != nil {
return
}
}
}
conn.ReadMessage(ctx)blocks until a message arrives and returns(MessageType, []byte, error)—websocket.TextMessagefor a JS string,websocket.BinaryMessagefor an ArrayBuffer. It returnswebsocket.ErrClosedafter the connection closes.conn.WriteMessage(mt, data)sends one message.conn.Close(code, reason)closes the connection;Close(0, "")callsclose()with no arguments. It is safe to call more than once.
Hibernatable WebSockets in a Durable Object
A Durable Object can accept hibernatable WebSockets, where messages arrive as separate triggers instead of an in-process read loop — so the object (and its Go state) can be evicted between messages. Use websocket.UpgradeHibernating, which does not call accept(), and hand the connection to state.AcceptHibernatingConn:
func (rm *Room) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.UpgradeHibernating(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// register under a tag so GetWebSockets can find it later; return at once
if err := rm.state.AcceptHibernatingConn(conn, "room"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Messages are then delivered to the object’s durableobjects.WebSocketMessageHandler (and optionally WebSocketCloseHandler/WebSocketErrorHandler) — possibly on a different wasm instance after hibernation:
func (rm *Room) WebSocketMessage(ctx context.Context, conn *websocket.HibernatingConn,
mt websocket.MessageType, data []byte) error {
peers, err := rm.state.GetWebSockets("room")
if err != nil {
return err
}
for _, p := range peers {
websocket.HibernatingConnFromJS(p).WriteMessage(mt, data)
}
return nil
}
*websocket.HibernatingConn supports WriteMessage and Close but has no ReadMessage. When forwarding an upgrade request from a plain worker to a Durable Object, use stub.FetchWebSocket(w, r) instead of stub.Fetch — a 101 response’s WebSocket pairing can’t ride along in a reconstructed *http.Response.
Example projects
- _examples/websocket-echo — a plain echo server using
Upgrade. - _examples/durable-object-websocket — a broadcast chat room using hibernatable WebSockets.