Skip to content
workers-go
Esc
navigateopen⌘Jpreview
On this page

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.TextMessage for a JS string, websocket.BinaryMessage for an ArrayBuffer. It returns websocket.ErrClosed after the connection closes.
  • conn.WriteMessage(mt, data) sends one message.
  • conn.Close(code, reason) closes the connection; Close(0, "") calls close() 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

Was this page helpful?