← Back to DevBytes

Implementing Web Transport Protocol in Modern Web Applications

Understanding the Web Transport Protocol

The Web Transport protocol is a modern web communication framework built on top of QUIC and HTTP/3, designed to supersede older technologies like WebSocket and Server-Sent Events for specific high-performance use cases. It provides a flexible API that supports multiple independent streams, unreliable datagrams, and reliable in-order delivery—all multiplexed over a single connection. This means you can send latency-sensitive game state updates as fire-and-forget datagrams while simultaneously streaming reliable file chunks or chat messages, all without head-of-line blocking interfering with your critical data flows.

Unlike WebSocket, which operates over a single TCP connection and suffers from head-of-line blocking when a packet is lost, Web Transport inherits QUIC's multiplexed stream architecture. Each stream is independently flow-controlled, so a stalled stream never blocks others. Additionally, Web Transport natively supports datagrams—small, unordered, unreliable messages—which are ideal for real-time positional data in multiplayer games, sensor telemetry, or live video frame acknowledgments.

Why Web Transport Matters for Modern Applications

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

The web has moved far beyond request-response cycles. Today's applications demand persistent, bidirectional, low-latency communication channels. Consider these scenarios where Web Transport shines:

The key architectural advantage is eliminating the TCP head-of-line blocking problem entirely. In a WebSocket connection, if a single TCP segment is dropped, every subsequent message—regardless of its logical independence—must wait for retransmission. With Web Transport over QUIC, only the affected stream stalls; all other streams and datagrams proceed unimpeded. This alone justifies the migration for latency-critical workloads.

Core Concepts and API Surface

Before diving into code, let's clarify the building blocks you'll work with:

All of these coexist within a single QUIC connection, sharing congestion control and encryption context while remaining fully independent.

Setting Up a Web Transport Server

To accept Web Transport connections, your server must speak HTTP/3 and understand the extended CONNECT method. Below is a production-oriented example using Node.js with the built-in QUIC support (available experimentally in Node.js 20+) and a self-signed certificate for local development.

Generating Certificates for Local Development

QUIC requires TLS 1.3. For localhost testing, generate a self-signed certificate:

openssl req -new -newkey rsa:2048 -nodes -keyout localhost.key -out localhost.csr -subj "/CN=localhost"
openssl x509 -req -days 365 -in localhost.csr -signkey localhost.key -out localhost.crt

Node.js Web Transport Server Implementation

// server.mjs - WebTransport server over HTTP/3 (QUIC)
import { createQuicSocket } from 'node:quic';
import { readFile } from 'node:fs/promises';
import { createReadableStream, createWritableStream } from 'node:stream/web';

const key = await readFile('./localhost.key');
const cert = await readFile('./localhost.crt');

const quicSocket = createQuicSocket({
  endpoint: { port: 4433 },
  serverName: 'localhost',
  key,
  cert,
  alpn: 'h3',
  maxStreamsBidirectional: 100,
  maxStreamsUnidirectional: 100,
  maxDatagramFrameSize: 4096
});

quicSocket.on('session', (session) => {
  console.log('New QUIC session established:', session.socketAddress);

  session.on('stream', (stream) => {
    // HTTP/3 request parsing on stream 0
    const decoder = new TextDecoder();
    let headers = '';
    stream.on('data', (chunk) => {
      headers += decoder.decode(chunk);
      if (headers.includes('\r\n\r\n')) {
        const headerLines = headers.split('\r\n');
        const methodLine = headerLines[0];
        
        if (methodLine.startsWith('CONNECT') && headerLines.some(l => l.toLowerCase().startsWith(':protocol: webtransport'))) {
          // Accept the WebTransport session
          stream.respond({
            status: 200,
            'sec-webtransport-http3-draft': 'draft-02'
          });
          stream.end();
          handleWebTransportSession(session);
        } else {
          stream.respond({ status: 405 });
          stream.end('Method Not Allowed');
        }
      }
    });
  });

  session.on('datagram', (datagram) => {
    // Handle incoming datagrams at session level
    const message = new TextDecoder().decode(datagram);
    console.log('Received datagram:', message);
  });

  session.on('close', () => {
    console.log('Session closed');
  });
});

async function handleWebTransportSession(session) {
  console.log('WebTransport session active');

  // Handle incoming bidirectional streams
  const incomingBidiStreams = session.incomingBidirectionalStreams;
  const reader = incomingBidiStreams.getReader();

  (async function processStreams() {
    while (true) {
      const { done, value: bidiStream } = await reader.read();
      if (done) break;
      
      // Each bidiStream has readable and writable halves
      const streamReader = bidiStream.readable.getReader();
      const streamWriter = bidiStream.writable.getWriter();
      
      // Echo back with prefix
      (async () => {
        try {
          while (true) {
            const { done: chunkDone, value: chunk } = await streamReader.read();
            if (chunkDone) break;
            const text = new TextDecoder().decode(chunk);
            console.log('Stream message:', text);
            await streamWriter.write(new TextEncoder().encode(`Echo: ${text}`));
          }
        } catch (err) {
          console.error('Stream error:', err);
        } finally {
          await streamWriter.close();
        }
      })();
    }
  })();

  // Send a datagram to the client every 2 seconds
  let counter = 0;
  const interval = setInterval(() => {
    const message = new TextEncoder().encode(`Server ping ${++counter}`);
    session.sendDatagram(message);
  }, 2000);

  session.on('close', () => {
    clearInterval(interval);
    console.log('WebTransport session ended');
  });
}

quicSocket.on('listening', () => {
  console.log('QUIC server listening on port 4433');
});

quicSocket.on('error', (err) => {
  console.error('QUIC socket error:', err);
});

// Start listening
quicSocket.listen();

This server listens for QUIC connections, detects WebTransport CONNECT requests, and establishes a session. Once active, it echoes any bidirectional stream data back to the client with an "Echo:" prefix and periodically sends datagrams containing server ping messages. The separation between stream handling and datagram broadcasting demonstrates the protocol's dual-mode nature.

Client-Side Web Transport API

The browser provides the WebTransport constructor. As of early 2025, it's available in Chrome (and Chromium-based browsers) behind a flag or enabled by default in recent versions. Always check feature detection before using it.

Feature Detection and Connection Establishment

// client.js - Browser-side WebTransport client
async function connectWebTransport() {
  // Feature detection
  if (!window.WebTransport) {
    console.error('WebTransport not supported in this browser');
    return;
  }

  const url = 'https://localhost:4433/webtransport';
  
  try {
    const transport = new WebTransport(url, {
      serverCertificateHashes: [
        {
          // SHA-256 fingerprint of your self-signed certificate
          algorithm: 'sha-256',
          value: new Uint8Array([
            // Replace with actual certificate fingerprint bytes
            0xab, 0xcd, 0xef, 0x01, /* ... truncated for brevity, use full 32 bytes */
          ])
        }
      ],
      // Optional: specify subprotocols
      requireUnreliable: true,  // Ensure datagram support
      allowPooling: false       // Disable connection reuse
    });

    await transport.ready;
    console.log('WebTransport connection established');
    return transport;
  } catch (error) {
    console.error('Connection failed:', error);
    throw error;
  }
}

Working with Bidirectional Streams

async function openBidirectionalStream(transport) {
  // Create a bidirectional stream
  const stream = await transport.createBidirectionalStream();
  
  // Get reader and writer
  const reader = stream.readable.getReader();
  const writer = stream.writable.getWriter();
  
  // Send a message to the server
  const encoder = new TextEncoder();
  await writer.write(encoder.encode('Hello from client!'));
  
  // Read server response
  const { value, done } = await reader.read();
  if (!done) {
    const responseText = new TextDecoder().decode(value);
    console.log('Server response:', responseText);
  }
  
  // Clean up
  await writer.close();
  reader.releaseLock();
  
  return stream;
}

// Receive incoming bidirectional streams from server
async function handleIncomingStreams(transport) {
  const incomingReader = transport.incomingBidirectionalStreams.getReader();
  
  while (true) {
    const { done, value: incomingStream } = await incomingReader.read();
    if (done) break;
    
    const reader = incomingStream.readable.getReader();
    const writer = incomingStream.writable.getWriter();
    
    // Process the incoming stream
    (async () => {
      try {
        while (true) {
          const { done: chunkDone, value: chunk } = await reader.read();
          if (chunkDone) break;
          console.log('Received from server stream:', new TextDecoder().decode(chunk));
          
          // Acknowledge receipt
          await writer.write(new TextEncoder().encode('ACK'));
        }
      } finally {
        await writer.close();
      }
    })();
  }
}

Sending and Receiving Datagrams

async function handleDatagrams(transport) {
  // Check if datagrams are supported
  if (transport.datagrams === null) {
    console.warn('Datagram support not negotiated');
    return;
  }

  // Get the datagram writer
  const writer = transport.datagrams.writable.getWriter();
  
  // Send a datagram (fire-and-forget, no reliability guarantee)
  const positionUpdate = new TextEncoder().encode(JSON.stringify({
    x: Math.random() * 100,
    y: Math.random() * 100,
    timestamp: Date.now()
  }));
  
  await writer.write(positionUpdate);
  console.log('Position datagram sent');
  
  // Keep writer open for future datagrams
  // writer.close() when done
  
  // Reading datagrams from server
  const reader = transport.datagrams.readable.getReader();
  
  (async function readDatagrams() {
    try {
      while (true) {
        const { done, value: datagram } = await reader.read();
        if (done) break;
        
        const message = new TextDecoder().decode(datagram);
        console.log('Received datagram:', message);
        
        // Datagrams are perfect for real-time metrics
        if (message.startsWith('Server ping')) {
          const latency = Date.now() - lastPingSentTime;
          console.log(`Estimated RTT: ${latency}ms`);
        }
      }
    } catch (err) {
      console.error('Datagram reader error:', err);
    }
  })();
  
  return { writer, reader };
}

Full Client Integration Example

// main-client.js - Complete WebTransport client lifecycle
async function main() {
  if (!window.WebTransport) {
    document.body.innerHTML = '

WebTransport not supported. Use Chrome 97+ with flags enabled.

'; return; } const statusEl = document.getElementById('status'); const outputEl = document.getElementById('output'); function log(message) { outputEl.textContent += `[${new Date().toISOString()}] ${message}\n`; outputEl.scrollTop = outputEl.scrollHeight; } statusEl.textContent = 'Connecting...'; try { const transport = new WebTransport('https://localhost:4433/webtransport', { serverCertificateHashes: [{ algorithm: 'sha-256', value: new Uint8Array(32).fill(0xab) // Replace with actual hash }] }); await transport.ready; statusEl.textContent = 'Connected'; log('WebTransport connection established'); // Set up incoming stream handler const incomingStreamsPromise = handleIncomingStreams(transport); // Set up datagram handler const datagramHandlerPromise = handleDatagrams(transport); // Open a bidirectional stream every 10 seconds const streamInterval = setInterval(async () => { try { const stream = await transport.createBidirectionalStream(); const writer = stream.writable.getWriter(); const reader = stream.readable.getReader(); await writer.write(new TextEncoder().encode(`Client message at ${Date.now()}`)); const { value, done } = await reader.read(); if (!done) { log(`Stream response: ${new TextDecoder().decode(value)}`); } await writer.close(); reader.releaseLock(); } catch (err) { log(`Stream error: ${err.message}`); } }, 10000); // Handle connection close transport.closed.then(() => { clearInterval(streamInterval); statusEl.textContent = 'Disconnected'; log('Connection closed'); }).catch((err) => { clearInterval(streamInterval); statusEl.textContent = 'Error'; log(`Connection error: ${err.message}`); }); // Graceful shutdown on page unload window.addEventListener('beforeunload', () => { transport.close(); }); } catch (error) { statusEl.textContent = 'Failed'; log(`Initial connection failed: ${error.message}`); } } // Start when page loads document.addEventListener('DOMContentLoaded', main);

Handling Unidirectional Streams

Unidirectional streams are perfect for server-initiated data pushes where the client doesn't need to respond on the same channel. They're lighter-weight than bidirectional streams and ideal for log streaming, telemetry broadcasts, or media delivery.

Server-Side Unidirectional Stream Creation

// Inside handleWebTransportSession function
async function openUnidirectionalStream(session, data) {
  const stream = await session.createUnidirectionalStream();
  const writer = stream.writable.getWriter();
  
  // Write data chunks
  const encoder = new TextEncoder();
  for (const chunk of data) {
    await writer.write(encoder.encode(JSON.stringify(chunk) + '\n'));
  }
  
  await writer.close();
  console.log('Unidirectional stream completed');
}

// Usage: send log batch to client
await openUnidirectionalStream(session, [
  { level: 'info', message: 'Server started' },
  { level: 'warn', message: 'Memory usage at 75%' },
  { level: 'info', message: 'New connection from 192.168.1.5' }
]);

Client-Side Unidirectional Stream Reception

async function handleIncomingUnidirectionalStreams(transport) {
  const reader = transport.incomingUnidirectionalStreams.getReader();
  
  while (true) {
    const { done, value: stream } = await reader.read();
    if (done) break;
    
    const streamReader = stream.getReader();
    let buffer = '';
    
    try {
      while (true) {
        const { done: chunkDone, value: chunk } = await streamReader.read();
        if (chunkDone) break;
        
        buffer += new TextDecoder().decode(chunk, { stream: true });
        
        // Process complete lines
        const lines = buffer.split('\n');
        buffer = lines.pop(); // Keep incomplete line in buffer
        
        for (const line of lines) {
          if (line.trim()) {
            const entry = JSON.parse(line);
            console.log(`[${entry.level}] ${entry.message}`);
          }
        }
      }
    } finally {
      streamReader.releaseLock();
    }
  }
}

Error Handling and Resilience Patterns

Web Transport connections can be terminated by network changes, server shutdowns, or QUIC session expiration. Unlike WebSocket's simple onclose event, Web Transport provides richer error information through the closed promise and individual stream termination.

Comprehensive Error Handling

class WebTransportManager {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.transport = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.activeStreams = new Set();
    this.datagramWriter = null;
  }

  async connect() {
    try {
      this.transport = new WebTransport(this.url, this.options);
      await this.transport.ready;
      
      this.reconnectAttempts = 0;
      console.log('Connected successfully');
      
      // Monitor closure
      this.transport.closed
        .then(() => this.onClose({ code: 0, message: 'Normal closure' }))
        .catch((error) => this.onClose(error));
      
      return this.transport;
    } catch (error) {
      console.error('Connection attempt failed:', error);
      return this.handleReconnect(error);
    }
  }

  async onClose(closeInfo) {
    console.log('Connection closed:', closeInfo);
    
    // Cancel all active streams
    for (const stream of this.activeStreams) {
      try {
        if (stream.writable) {
          await stream.writable.getWriter().close().catch(() => {});
        }
        if (stream.readable) {
          stream.readable.getReader().releaseLock();
        }
      } catch (e) {
        // Stream already closed
      }
    }
    this.activeStreams.clear();
    this.datagramWriter = null;
    
    // Attempt reconnection if appropriate
    if (closeInfo.code !== 0 && this.reconnectAttempts < this.maxReconnectAttempts) {
      return this.handleReconnect(closeInfo);
    }
  }

  async handleReconnect(error) {
    this.reconnectAttempts++;
    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
    
    console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
    
    await new Promise(resolve => setTimeout(resolve, delay));
    return this.connect();
  }

  async createStreamWithRetry(maxRetries = 3) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        if (!this.transport) {
          await this.connect();
        }
        
        const stream = await this.transport.createBidirectionalStream();
        this.activeStreams.add(stream);
        
        // Auto-cleanup on stream close
        Promise.all([
          stream.readable.getReader().closed.catch(() => {}),
          stream.writable.getWriter().closed.catch(() => {})
        ]).then(() => {
          this.activeStreams.delete(stream);
        });
        
        return stream;
      } catch (err) {
        if (attempt === maxRetries - 1) throw err;
        await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
      }
    }
  }
}

// Usage
const manager = new WebTransportManager('https://localhost:4433/webtransport', {
  serverCertificateHashes: [{ algorithm: 'sha-256', value: new Uint8Array(32) }],
  maxReconnectAttempts: 10,
  reconnectDelay: 500
});

await manager.connect();
const stream = await manager.createStreamWithRetry();

Stream Timeouts and Cancellation

async function createStreamWithTimeout(transport, timeoutMs = 5000) {
  const stream = await transport.createBidirectionalStream();
  const writer = stream.writable.getWriter();
  const reader = stream.readable.getReader();
  
  // Set up timeout
  const timeoutId = setTimeout(async () => {
    console.warn('Stream timeout - aborting');
    await writer.abort(new Error('Stream timed out'));
    reader.cancel('Timeout');
  }, timeoutMs);
  
  try {
    // Attempt read with a deadline
    const result = await Promise.race([
      reader.read(),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error('Read deadline exceeded')), timeoutMs)
      )
    ]);
    
    clearTimeout(timeoutId);
    
    if (!result.done) {
      const text = new TextDecoder().decode(result.value);
      console.log('Received within deadline:', text);
    }
    
    return result;
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  } finally {
    await writer.close().catch(() => {});
    reader.releaseLock();
  }
}

Performance Tuning and Best Practices

1. Choose the Right Transport Mode for Your Data

Not all data requires the same delivery guarantees. Classify your application's messages into three categories and map them appropriately:

2. Implement Backpressure Handling

QUIC streams have built-in flow control. If your writer outpaces the reader, the stream will exert backpressure. Always respect this in your write loops:

async function writeWithBackpressure(writer, chunks) {
  for (const chunk of chunks) {
    // Wait for the writer to be ready before writing
    // The writer's desiredSize property indicates readiness
    if (writer.desiredSize <= 0) {
      console.log('Backpressure detected, waiting...');
      await writer.ready;
    }
    
    await writer.write(chunk);
  }
  await writer.close();
}

3. Minimize Stream Creation Overhead

Creating a new stream involves a QUIC-level frame exchange. For high-frequency, small-payload communication, reuse existing streams or use datagrams instead of opening a new stream for each message. A good rule of thumb: if you're opening more than 10 streams per second for tiny payloads, switch to datagrams or multiplex on a single stream with a framing protocol.

4. Certificate Management in Production

During development, serverCertificateHashes allows bypassing certificate validation with a known fingerprint. In production, always use a valid TLS certificate from a trusted CA. The browser will enforce standard TLS verification for HTTPS origins. For local testing across devices, consider using a local CA like mkcert:

# Install mkcert and generate trusted local certificates
mkcert -install
mkcert localhost 192.168.1.100 ::1
# This produces localhost+2.pem and localhost+2-key.pem

5. Monitor Connection Health Proactively

QUIC connections can silently degrade due to NAT timeouts or network changes. Implement a heartbeat using datagrams:

// Client-side heartbeat
let lastServerPing = Date.now();
const HEARTBEAT_TIMEOUT = 10000; // 10 seconds

async function monitorConnectionHealth(transport) {
  const reader = transport.datagrams.readable.getReader();
  
  setInterval(() => {
    if (Date.now() - lastServerPing > HEARTBEAT_TIMEOUT) {
      console.error('Connection appears dead, triggering reconnect...');
      transport.close();
    }
  }, 2000);
  
  (async () => {
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        lastServerPing = Date.now();
      }
    } catch (err) {
      console.error('Health monitor error:', err);
    }
  })();
}

6. Graceful Degradation for Unsupported Browsers

Not all browsers support Web Transport yet. Always provide a fallback path:

async function createTransport(url, options) {
  if (window.WebTransport) {
    try {
      const transport = new WebTransport(url, options);
      await transport.ready;
      console.log('Using WebTransport');
      return { transport, type: 'webtransport' };
    } catch (err) {
      console.warn('WebTransport failed, falling back:', err.message);
    }
  }
  
  // Fallback to WebSocket
  const wsUrl = url.replace('https://', 'wss://').replace('/webtransport', '/ws');
  const ws = new WebSocket(wsUrl);
  
  await new Promise((resolve, reject) => {
    ws.onopen = resolve;
    ws.onerror = reject;
  });
  
  console.log('Using WebSocket fallback');
  return { transport: ws, type: 'websocket' };
}

// Unified send abstraction
async function sendMessage(connection, data, reliable = true) {
  if (connection.type === 'webtransport') {
    if (reliable) {
      const stream = await connection.transport.createBidirectionalStream();
      const writer = stream.writable.getWriter();
      await writer.write(new TextEncoder().encode(JSON.stringify(data)));
      await writer.close();
    } else {
      const writer = connection.transport.datagrams.writable.getWriter();
      await writer.write(new TextEncoder().encode(JSON.stringify(data)));
    }
  } else {
    // WebSocket fallback
    connection.transport.send(JSON.stringify(data));
  }
}

7. Security Considerations

Debugging and Diagnostics

Debugging QUIC-based protocols requires different tools than traditional HTTP debugging. Here are essential diagnostic approaches:

// Client-side diagnostic logging
async function connectWithDiagnostics(url, options) {
  const transport = new WebTransport(url, options);
  
  // Log session-level events
  transport.ready.then(() => {
    console.log('[WebTransport] Session ready');
    console.log('[WebTransport] Incoming streams:', 
      transport.incomingBidirectionalStreams ? 'available' : 'unavailable');
    console.log('[WebTransport] Datagrams:', 
      transport.datagrams ? 'supported' : 'unsupported');
  });
  
  // Monitor stream creation performance
  const startTime = performance.now();
  const stream = await transport.createBidirectionalStream();
  const createTime = performance.now() - startTime;
  console.log(`[WebTransport] Stream creation latency: ${createTime.toFixed(2)}ms`);
  
  // Track datagram round-trip time
  const pingId = crypto.randomUUID();
  const sendTime = performance.now();
  
  const writer = transport.datagrams.writable.getWriter();
  await writer.write(new TextEncoder().encode(`ping:${pingId}:${sendTime}`));
  
  const reader = transport.datagrams.readable.getReader();
  (async () => {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const msg = new TextDecoder().decode(value);
      if (msg.startsWith(`pong:${pingId}`)) {
        const rtt = performance.now() - sendTime;
        console.log(`[WebTransport] Datagram RTT: ${rtt.toFixed(2)}ms`);
      }
    }
  })();
  
  return transport;
}

For server-side debugging, use QUIC-aware tools like qlog (QUIC logging format) and Wireshark with QUIC dissector enabled. Chrome's chrome://webrtc-internals can also provide insight into the underlying QUIC connection when Web Transport is used.

Complete Production-Ready Example: Real-Time Dashboard

Let's tie everything together with a complete example: a real-time server monitoring dashboard that uses datagrams for instantaneous CPU/memory metrics and bidirectional streams for configuration commands.

Server Implementation (Full)

// dashboard-server.mjs
import { createQuicSocket } from 'node:quic';
import { readFile } from 'node:fs/promises';
import os from 'node:os';

const key = await readFile('./localhost-key.pem');
const cert = await readFile('./localhost.pem');

const sessions = new Map(); // Track active sessions

const quicSocket = createQuicSocket({
  endpoint: { port: 4433 },
  serverName: 'localhost',
  key,
  cert,
  alpn: 'h3',
  maxStreamsBidirectional: 50,
  maxStreamsUnidirectional: 50,
  maxDatagramFrameSize: 4096
});

quicSocket.on('session', (session) => {
  const sessionId = crypto.randomUUID();
  
  session.on('stream', (stream) => {
    const decoder = new TextDecoder();
    let headers = '';
    
    stream.on('data', (chunk) => {
      headers += decoder.decode(chunk);
      if (headers.includes('\r\n\r\n')) {
        const lines = headers.split('\r\n');
        const methodLine = lines[0];
        
        if (methodLine.startsWith('CONNECT') && 
            lines.some(l => l.toLowerCase().startsWith(':protocol: webtransport'))) {
          stream.respond({
            status: 200,
            'sec-webtransport-http3-draft': 'draft-02'
          });
          stream.end();
          
          // Initialize session
          sessions.set(sessionId, {
            session,
            metricsInterval: null,
            createdAt: Date.now()
          });
          
          handleDashboardSession(session, sessionId);
        } else {
          stream.respond({ status: 405 });
          stream.end();
        }
      }
    });
  });
  
  session.on('close', () => {
    const sessionData = sessions.get(sessionId);
    if (sessionData?.metricsInterval) {
      clearInterval(sessionData.metricsInterval);
    }
    sessions.delete(sessionId);
  });
});

async function handleDashboardSession(session, sessionId) {
  const sessionData = sessions.get(sessionId);
  
  // Send system metrics as datagrams every second
  sessionData.metricsInterval = setInterval(() => {
    const metrics = {
      type: 'metrics',
      cpu: (os.loadavg()[0] * 100).toFixed(1),
      memory: ((1 - os.freemem() / os.totalmem()) * 100).toFixed(1),
      uptime: os.uptime(),
      timestamp: Date.now()
    };
    
    const payload = new TextEncoder().encode(JSON.stringify(metrics));
    session.sendDatagram(payload);

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles