How to Implement WebSocket Communication in Deno: Client and Server Guide

WebSocket communication in Deno is implemented using the native global WebSocket class for client connections and Deno.upgradeWebSocket() for server-side upgrades, both sharing the same internal prototype defined in ext/websocket/01_websocket.js and ext/http/02_websocket.ts.

Deno provides a full-featured, browser-compatible WebSocket implementation that requires no external dependencies. According to the denoland/deno source code, the WebSocket stack is split between client-side logic in ext/websocket/01_websocket.js and server-side upgrade handling in ext/http/02_websocket.ts, enabling bidirectional, event-driven communication for real-time applications.

Understanding Deno's WebSocket Architecture

Deno's WebSocket implementation consists of two primary layers that share a common JavaScript class but handle connection establishment differently based on their role.

Client-Side WebSocket Class

The global WebSocket constructor available in Deno is implemented in ext/websocket/01_websocket.js. This class handles the entire client lifecycle:

  • URL parsing – The constructor forces schemes to ws: or wss: (lines 53-95)
  • Permission validation – Creates a cancel handle via op_ws_check_permission_and_cancel_handle (lines 73-78)
  • Resource creation – Spawns a native handle through op_ws_create (lines 81-90)
  • Event polling – The internal [_eventLoop] continuously polls op_ws_next_event to dispatch "message", "close", "error", and "pong" events (lines 108-180)

The only difference between client and server instances is the role (CLIENT vs SERVER) and how the resource identifier (rid) is obtained.

Server-Side Upgrade Mechanism

For servers, Deno.upgradeWebSocket() in ext/http/02_websocket.ts handles the HTTP upgrade handshake:

  1. Validates that the Upgrade header contains "websocket" and the Connection header contains "Upgrade" (lines 42-58)
  2. Generates the Sec-WebSocket-Accept header via op_http_websocket_accept_header (line 67)
  3. Handles optional sec-websocket-protocol negotiation (lines 76-89)
  4. Creates a server-side socket using createWebSocketBranded with role SERVER and an idle-timeout value (default 30s) (lines 91-98)
  5. Returns a 101 Switching Protocols Response object alongside the socket (lines 69-75)

Implementing a WebSocket Client

To establish a client connection, instantiate the global WebSocket class with a WebSocket URL:

// client.ts
const ws = new WebSocket("wss://echo.deno.dev");

// Connection established
ws.addEventListener("open", () => {
  console.log("Connected");
  ws.send("Hello Deno WebSocket!");
});

// Message from server
ws.addEventListener("message", (e) => {
  console.log("Received:", e.data);
  ws.close(1000, "Done");
});

// Close event
ws.addEventListener("close", (e) => {
  console.log(`Closed (code=${e.code}, reason=${e.reason})`);
});

// Error handling
ws.addEventListener("error", (e) => {
  console.error("WebSocket error:", e);
});

The constructor in ext/websocket/01_websocket.js automatically transitions the readyState to OPEN upon handshake completion and dispatches the "open" event. All message framing and queuing is handled internally by the runtime's Rust-backed operations.

Building a WebSocket Server

Use Deno.upgradeWebSocket() within an HTTP handler to upgrade connections:

// server.ts
Deno.serve(async (req) => {
  const upgrade = req.headers.get("upgrade") ?? "";
  if (upgrade.toLowerCase() !== "websocket") {
    return new Response("Not a WebSocket request", { status: 400 });
  }

  // Perform the upgrade (calls ext/http/02_websocket.ts)
  const { socket, response } = Deno.upgradeWebSocket(req, {
    idleTimeout: 60, // seconds, default is 30
  });

  // Handle incoming messages
  socket.addEventListener("message", (e) => {
    console.log("Received:", e.data);
    socket.send(`Echo: ${e.data}`);
  });

  socket.addEventListener("close", (e) => {
    console.log(`Connection closed (code=${e.code})`);
  });

  // Return 101 Switching Protocols response
  return response;
});

The upgradeWebSocket function returns an object containing:

  • socket: A WebSocket instance (same prototype as client-side but with role SERVER)
  • response: A Response object with status 101 containing the required handshake headers

The server's open event is emitted once the internal _wantsUpgrade promise resolves to a native rid, completing the asynchronous upgrade process (lines 99-124 in ext/http/02_websocket.ts).

Advanced Configuration Options

Idle Timeout Management

Server-side WebSockets include automatic idle timeout handling implemented in ext/websocket/01_websocket.js via [_serverHandleIdleTimeout] (lines 154-190). This feature sends periodic ping frames and automatically closes the connection after the specified period of inactivity, preventing resource exhaustion from stale connections.

Custom HTTP Clients

For advanced networking scenarios, you can pass a custom HttpClient to the WebSocket constructor for proxy support or custom TLS configuration:

import { HttpClient } from "https://deno.land/std@0.228.0/http/client.ts";

const client = new HttpClient({ proxy: { url: "http://my-proxy:3128" } });
const ws = new WebSocket("ws://example.com/socket", { client });

ws.onopen = () => console.log("Connected via proxy");
ws.onmessage = (e) => console.log("Message:", e.data);

The client option is validated in ext/websocket/01_websocket.js (lines 22-42), and the underlying resource ID is passed directly to op_ws_create.

Summary

  • Deno provides native WebSocket support through the global WebSocket class and Deno.upgradeWebSocket() without requiring external libraries.
  • Client and server share the same prototype defined in ext/websocket/01_websocket.js, differing only in their role (CLIENT vs SERVER) and connection establishment method.
  • Server upgrades require header validation for Upgrade: websocket and Connection: Upgrade, plus generation of Sec-WebSocket-Accept via op_http_websocket_accept_header in ext/http/02_websocket.ts.
  • Automatic idle timeout handling protects server resources by closing inactive connections after a configurable period (default 30 seconds).
  • Type definitions for the global WebSocket and Deno.upgradeWebSocket are located in cli/tsc/dts/lib.deno_websocket.d.ts.

Frequently Asked Questions

What is the difference between client and server WebSocket instances in Deno?

Client and server WebSocket instances use the exact same class implementation from ext/websocket/01_websocket.js and share the same prototype methods (send, close, addEventListener). The only internal differences are the role property (CLIENT vs SERVER) and how the resource identifier (rid) is obtained—clients create resources directly via op_ws_create, while servers receive their rid through the HTTP upgrade mechanism in ext/http/02_websocket.ts.

How does Deno handle WebSocket ping/pong and idle timeouts?

According to ext/websocket/01_websocket.js, server-side sockets implement [_serverHandleIdleTimeout] (lines 154-190) which automatically sends ping frames at intervals and closes the connection with code 1001 if no pong response is received within the configured timeout period (default 30 seconds). This prevents hanging connections from consuming server resources.

Can I use WebSocket with Deno.serve()?

Yes. Deno.serve() is fully compatible with Deno.upgradeWebSocket(). Pass the incoming Request object to upgradeWebSocket() within your handler, then return the resulting response to complete the 101 Switching Protocols handshake. The socket begins emitting events immediately after the upgrade completes.

What permissions are required for WebSocket connections in Deno?

Client WebSocket connections require the --allow-net permission for the target host and port. According to the source code in ext/websocket/01_websocket.js (lines 73-78), Deno invokes op_ws_check_permission_and_cancel_handle during construction to validate network access before establishing the connection. Server-side WebSockets inherit the permissions from the HTTP server itself.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →