Configure Deno WebSocket connections for real-time applications with clustering and production deployment

Intermediate 45 min Apr 11, 2026 32 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up production-ready Deno WebSocket servers with authentication, clustering, and load balancing for real-time applications. Complete with systemd service configuration and NGINX reverse proxy setup.

Prerequisites

  • Root or sudo access
  • Basic knowledge of JavaScript/TypeScript
  • Understanding of WebSocket protocol
  • Familiarity with systemd services

What this solves

Real-time applications need WebSocket connections to handle bidirectional communication between clients and servers. Deno provides excellent WebSocket support, but production deployments require proper clustering, authentication, and load balancing. This tutorial shows you how to build a scalable WebSocket server with Deno that handles multiple connections, implements security middleware, and deploys with high availability.

Step-by-step installation

Install Deno runtime

Download and install the latest Deno version directly from the official repository.

curl -fsSL https://deno.land/x/install/install.sh | sh

Add Deno to your system PATH for all users.

sudo mv ~/.deno/bin/deno /usr/local/bin/
deno --version

Create WebSocket server directory structure

Set up the application directory with proper permissions for the deployment user.

sudo mkdir -p /opt/websocket-server
sudo chown $USER:$USER /opt/websocket-server
cd /opt/websocket-server
mkdir -p src middleware config logs

Build WebSocket server with connection handling

Create the main server file with WebSocket upgrade handling and broadcasting capabilities.

import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { authenticateConnection } from "./middleware/auth.ts";
import { WebSocketManager } from "./websocket-manager.ts";

const PORT = parseInt(Deno.env.get("PORT") || "8080");
const wsManager = new WebSocketManager();

async function handler(req: Request): Promise {
  const { pathname } = new URL(req.url);
  
  if (pathname === "/ws") {
    const upgrade = req.headers.get("upgrade") || "";
    if (upgrade.toLowerCase() !== "websocket") {
      return new Response("Request must be a WebSocket upgrade", { status: 426 });
    }
    
    // Authenticate connection before upgrade
    const authResult = await authenticateConnection(req);
    if (!authResult.success) {
      return new Response("Unauthorized", { status: 401 });
    }
    
    const { socket, response } = Deno.upgradeWebSocket(req);
    wsManager.handleConnection(socket, authResult.userId!);
    return response;
  }
  
  if (pathname === "/health") {
    return new Response(JSON.stringify({
      status: "healthy",
      connections: wsManager.getConnectionCount(),
      uptime: Date.now() - startTime
    }), {
      headers: { "Content-Type": "application/json" }
    });
  }
  
  return new Response("Not found", { status: 404 });
}

const startTime = Date.now();
console.log(WebSocket server starting on port ${PORT});
serve(handler, { port: PORT });

Implement WebSocket connection manager

Create a manager class to handle multiple connections, broadcasting, and cleanup.

interface Connection {
  socket: WebSocket;
  userId: string;
  lastPing: number;
  rooms: Set;
}

interface Message {
  type: string;
  room?: string;
  data: any;
  userId?: string;
}

export class WebSocketManager {
  private connections = new Map();
  private rooms = new Map>();
  private pingInterval: number;
  
  constructor() {
    this.pingInterval = setInterval(() => this.pingConnections(), 30000);
  }
  
  handleConnection(socket: WebSocket, userId: string): void {
    const connectionId = crypto.randomUUID();
    const connection: Connection = {
      socket,
      userId,
      lastPing: Date.now(),
      rooms: new Set()
    };
    
    this.connections.set(connectionId, connection);
    console.log(User ${userId} connected (${connectionId}));
    
    socket.onmessage = (event) => this.handleMessage(connectionId, event);
    socket.onclose = () => this.handleDisconnection(connectionId);
    socket.onerror = (error) => console.error(WebSocket error for ${connectionId}:, error);
    
    // Send welcome message
    this.sendToConnection(connectionId, {
      type: "connected",
      data: { connectionId, userId }
    });
  }
  
  private handleMessage(connectionId: string, event: MessageEvent): void {
    const connection = this.connections.get(connectionId);
    if (!connection) return;
    
    try {
      const message: Message = JSON.parse(event.data);
      connection.lastPing = Date.now();
      
      switch (message.type) {
        case "join_room":
          this.joinRoom(connectionId, message.data.room);
          break;
        case "leave_room":
          this.leaveRoom(connectionId, message.data.room);
          break;
        case "broadcast":
          this.broadcastToRoom(message.room!, {
            type: "message",
            data: message.data,
            userId: connection.userId
          }, connectionId);
          break;
        case "ping":
          this.sendToConnection(connectionId, { type: "pong", data: {} });
          break;
      }
    } catch (error) {
      console.error(Invalid message from ${connectionId}:, error);
    }
  }
  
  private handleDisconnection(connectionId: string): void {
    const connection = this.connections.get(connectionId);
    if (!connection) return;
    
    // Leave all rooms
    connection.rooms.forEach(room => this.leaveRoom(connectionId, room));
    this.connections.delete(connectionId);
    console.log(User ${connection.userId} disconnected (${connectionId}));
  }
  
  private joinRoom(connectionId: string, room: string): void {
    const connection = this.connections.get(connectionId);
    if (!connection) return;
    
    connection.rooms.add(room);
    if (!this.rooms.has(room)) {
      this.rooms.set(room, new Set());
    }
    this.rooms.get(room)!.add(connectionId);
    
    this.sendToConnection(connectionId, {
      type: "room_joined",
      data: { room }
    });
  }
  
  private leaveRoom(connectionId: string, room: string): void {
    const connection = this.connections.get(connectionId);
    if (!connection) return;
    
    connection.rooms.delete(room);
    const roomConnections = this.rooms.get(room);
    if (roomConnections) {
      roomConnections.delete(connectionId);
      if (roomConnections.size === 0) {
        this.rooms.delete(room);
      }
    }
    
    this.sendToConnection(connectionId, {
      type: "room_left",
      data: { room }
    });
  }
  
  private broadcastToRoom(room: string, message: Message, excludeConnectionId?: string): void {
    const roomConnections = this.rooms.get(room);
    if (!roomConnections) return;
    
    roomConnections.forEach(connectionId => {
      if (connectionId !== excludeConnectionId) {
        this.sendToConnection(connectionId, message);
      }
    });
  }
  
  private sendToConnection(connectionId: string, message: Message): void {
    const connection = this.connections.get(connectionId);
    if (!connection || connection.socket.readyState !== WebSocket.OPEN) return;
    
    try {
      connection.socket.send(JSON.stringify(message));
    } catch (error) {
      console.error(Failed to send message to ${connectionId}:, error);
      this.handleDisconnection(connectionId);
    }
  }
  
  private pingConnections(): void {
    const now = Date.now();
    const timeout = 60000; // 1 minute timeout
    
    this.connections.forEach((connection, connectionId) => {
      if (now - connection.lastPing > timeout) {
        console.log(Connection ${connectionId} timed out);
        connection.socket.close();
        this.handleDisconnection(connectionId);
      } else if (connection.socket.readyState === WebSocket.OPEN) {
        this.sendToConnection(connectionId, { type: "ping", data: {} });
      }
    });
  }
  
  getConnectionCount(): number {
    return this.connections.size;
  }
  
  getRoomCount(): number {
    return this.rooms.size;
  }
}

Create authentication middleware

Implement JWT-based authentication for WebSocket connections with token validation.

import { verify } from "https://deno.land/x/djwt@v3.0.1/mod.ts";

interface AuthResult {
  success: boolean;
  userId?: string;
  error?: string;
}

const JWT_SECRET = Deno.env.get("JWT_SECRET") || "your-secret-key-change-this";

export async function authenticateConnection(req: Request): Promise {
  try {
    // Try to get token from Authorization header
    const authHeader = req.headers.get("Authorization");
    let token: string | null = null;
    
    if (authHeader && authHeader.startsWith("Bearer ")) {
      token = authHeader.substring(7);
    } else {
      // Try to get token from URL parameters
      const url = new URL(req.url);
      token = url.searchParams.get("token");
    }
    
    if (!token) {
      return { success: false, error: "No token provided" };
    }
    
    // Verify JWT token
    const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(JWT_SECRET),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign", "verify"]
    );
    
    const payload = await verify(token, key);
    
    if (!payload.sub) {
      return { success: false, error: "Invalid token payload" };
    }
    
    return { success: true, userId: payload.sub as string };
  } catch (error) {
    console.error("Authentication error:", error);
    return { success: false, error: "Token verification failed" };
  }
}

// Rate limiting middleware
const connectionAttempts = new Map();

export function rateLimitConnection(req: Request): boolean {
  const clientIP = req.headers.get("x-forwarded-for") || "unknown";
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxAttempts = 10;
  
  const attempts = connectionAttempts.get(clientIP) || { count: 0, lastAttempt: 0 };
  
  // Reset counter if window expired
  if (now - attempts.lastAttempt > windowMs) {
    attempts.count = 0;
  }
  
  attempts.count++;
  attempts.lastAttempt = now;
  connectionAttempts.set(clientIP, attempts);
  
  // Clean up old entries
  if (Math.random() < 0.01) { // 1% chance to clean up
    connectionAttempts.forEach((value, key) => {
      if (now - value.lastAttempt > windowMs) {
        connectionAttempts.delete(key);
      }
    });
  }
  
  return attempts.count <= maxAttempts;
}

Create environment configuration

Set up environment variables for production deployment with security settings.

PORT=8080
JWT_SECRET=your-very-secure-secret-key-change-this-in-production
NODE_ENV=production
LOG_LEVEL=info
MAX_CONNECTIONS=1000
PING_INTERVAL=30000
CONNECTION_TIMEOUT=60000

Create clustering script for load balancing

Implement a cluster manager to run multiple Deno processes for high availability.

import { serve } from "https://deno.land/std@0.208.0/http/server.ts";

const CLUSTER_SIZE = parseInt(Deno.env.get("CLUSTER_SIZE") || "4");
const BASE_PORT = parseInt(Deno.env.get("BASE_PORT") || "8080");
const workers: Deno.ChildProcess[] = [];

async function startWorker(port: number): Promise {
  console.log(Starting worker on port ${port});
  
  const env = { ...Deno.env.toObject(), PORT: port.toString() };
  
  const worker = new Deno.Command("deno", {
    args: ["run", "--allow-net", "--allow-env", "--allow-read", "src/server.ts"],
    env,
    stdout: "piped",
    stderr: "piped",
    cwd: "/opt/websocket-server"
  }).spawn();
  
  // Log worker output
  const decoder = new TextDecoder();
  worker.stdout.pipeTo(new WritableStream({
    write(chunk) {
      console.log([Worker ${port}]:, decoder.decode(chunk));
    }
  }));
  
  worker.stderr.pipeTo(new WritableStream({
    write(chunk) {
      console.error([Worker ${port} ERROR]:, decoder.decode(chunk));
    }
  }));
  
  return worker;
}

async function startCluster(): Promise {
  console.log(Starting cluster with ${CLUSTER_SIZE} workers);
  
  for (let i = 0; i < CLUSTER_SIZE; i++) {
    const port = BASE_PORT + i;
    const worker = await startWorker(port);
    workers.push(worker);
    
    // Add delay between worker starts
    if (i < CLUSTER_SIZE - 1) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

// Handle graceful shutdown
Deno.addSignalListener("SIGTERM", () => {
  console.log("Received SIGTERM, shutting down cluster...");
  workers.forEach(worker => worker.kill("SIGTERM"));
  Deno.exit(0);
});

Deno.addSignalListener("SIGINT", () => {
  console.log("Received SIGINT, shutting down cluster...");
  workers.forEach(worker => worker.kill("SIGINT"));
  Deno.exit(0);
});

// Start the cluster
if (import.meta.main) {
  await startCluster();
  
  // Keep the main process alive
  setInterval(() => {
    console.log(Cluster running with ${workers.length} workers);
  }, 30000);
}

Configure systemd service

Create a systemd service file for automatic startup and process management.

[Unit]
Description=Deno WebSocket Server
After=network.target
Wants=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/websocket-server
EnvironmentFile=/opt/websocket-server/.env
ExecStart=/usr/local/bin/deno run --allow-net --allow-env --allow-read cluster.ts
Restart=always
RestartSec=10
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30
LimitNOFILE=65536

Security settings

NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/opt/websocket-server/logs

Logging

StandardOutput=journal StandardError=journal SyslogIdentifier=websocket-server [Install] WantedBy=multi-user.target

Set proper ownership and permissions for the service files.

sudo chown -R www-data:www-data /opt/websocket-server
sudo chmod 755 /opt/websocket-server
sudo chmod 644 /opt/websocket-server/.env
sudo systemctl daemon-reload
sudo systemctl enable websocket-server

Configure NGINX reverse proxy

Set up NGINX to handle WebSocket proxying with load balancing across multiple Deno processes.

sudo apt update
sudo apt install -y nginx
sudo dnf install -y nginx

Create the NGINX configuration with WebSocket support and upstream load balancing.

upstream websocket_backend {
    least_conn;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8083 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    server_name example.com www.example.com;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=ws_limit:10m rate=10r/s;
    
    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    
    location /ws {
        limit_req zone=ws_limit burst=20 nodelay;
        
        # WebSocket proxy settings
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # WebSocket specific timeouts
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_connect_timeout 10s;
        
        # Prevent proxy buffering
        proxy_buffering off;
    }
    
    location /health {
        proxy_pass http://websocket_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location / {
        return 404;
    }
}

HTTPS configuration (add SSL certificates)

server { listen 443 ssl http2; server_name example.com www.example.com; # SSL configuration ssl_certificate /etc/ssl/certs/example.com.crt; ssl_certificate_key /etc/ssl/private/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; # Rate limiting limit_req_zone $binary_remote_addr zone=wss_limit:10m rate=10r/s; # Security headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; location /ws { limit_req zone=wss_limit burst=20 nodelay; proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_connect_timeout 10s; proxy_buffering off; } location /health { proxy_pass http://websocket_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

Enable the site and restart NGINX.

sudo ln -s /etc/nginx/sites-available/websocket-server /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Set up monitoring and logging

Create log rotation and monitoring scripts to track WebSocket connections and performance.

/opt/websocket-server/logs/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 644 www-data www-data
    postrotate
        systemctl reload websocket-server
    endscript
}

Create a monitoring script to check connection health.

#!/bin/bash

WebSocket server monitoring script

LOG_FILE="/opt/websocket-server/logs/monitor.log" HEALTH_ENDPOINT="http://localhost:8080/health" ALERT_THRESHOLD=1000 log_message() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" } check_health() { response=$(curl -s "$HEALTH_ENDPOINT" || echo "{}") connections=$(echo "$response" | jq -r '.connections // 0') status=$(echo "$response" | jq -r '.status // "unknown"') log_message "Health check: status=$status, connections=$connections" if [ "$status" != "healthy" ]; then log_message "ERROR: Service unhealthy" return 1 fi if [ "$connections" -gt "$ALERT_THRESHOLD" ]; then log_message "WARNING: High connection count: $connections" fi return 0 } if ! check_health; then log_message "Restarting websocket-server service" systemctl restart websocket-server fi

Make the script executable and add to cron.

sudo chmod +x /opt/websocket-server/monitor.sh
sudo chown www-data:www-data /opt/websocket-server/monitor.sh
echo "/5    * /opt/websocket-server/monitor.sh" | sudo crontab -u www-data -

Start and enable services

Start the WebSocket server and ensure it starts on boot.

sudo systemctl start websocket-server
sudo systemctl status websocket-server
sudo systemctl enable nginx
sudo systemctl restart nginx

Verify your setup

Test the WebSocket server health endpoint and connection handling.

# Check service status
sudo systemctl status websocket-server

Test health endpoint

curl http://localhost:8080/health

Check NGINX proxy

curl -H "Host: example.com" http://localhost/health

View service logs

sudo journalctl -u websocket-server -f

Check connection count

ss -tulpn | grep :808

Test WebSocket connections using a simple client script.

const ws = new WebSocket('wss://example.com/ws?token=your-jwt-token');

ws.onopen = function() {
    console.log('Connected to WebSocket');
    ws.send(JSON.stringify({ type: 'join_room', data: { room: 'test' } }));
};

ws.onmessage = function(event) {
    console.log('Received:', JSON.parse(event.data));
};

ws.onclose = function() {
    console.log('WebSocket connection closed');
};

Common issues

SymptomCauseFix
Service won't startPermission issuessudo chown -R www-data:www-data /opt/websocket-server
WebSocket upgrade failsNGINX proxy configCheck proxy_set_header Upgrade and Connection settings
High memory usageConnection leaksReview ping/pong implementation and connection cleanup
Authentication failuresJWT secret mismatchVerify JWT_SECRET environment variable matches token issuer
Load balancer not workingUpstream server downsudo systemctl restart websocket-server
SSL termination issuesCertificate configurationCheck certificate paths and NGINX SSL configuration
Never use chmod 777. It gives every user on the system full access to your files. Instead, fix ownership with chown and use minimal permissions like 755 for directories and 644 for files.

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.