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:
- Multiplayer gaming: Player position updates must arrive with minimal latency; retransmitting stale positional data wastes bandwidth and creates jitter. Datagrams solve this elegantly.
- Live streaming and conferencing: Audio, video, text chat, and screen-sharing control signals can each occupy their own stream, with different reliability and priority settings.
- IoT sensor fleets: Thousands of devices can maintain a single QUIC connection, sending telemetry over multiple streams without the overhead of opening new TLS handshakes.
- Collaborative editing: Document state sync can run over reliable streams while cursor position broadcasts use datagrams for zero-rebuffering fluidity.
- Edge computing and CDN interactivity: HTTP/3's 0-RTT handshake combined with Web Transport's multiplexing enables near-instantaneous stateful communication with edge nodes.
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:
- WebTransport connection: Established via an HTTP/3 extended CONNECT request (or sometimes HTTP/2 with prior knowledge). The browser's
WebTransportconstructor initiates this. - Bidirectional streams: Ordered, reliable, TCP-like channels within the connection. Created with
createBidirectionalStream()or received via theincomingBidirectionalStreamsreadable stream. - Unidirectional streams: One-way reliable streams, useful for server-to-client data pushes where acknowledgment isn't required.
- Datagrams: Unreliable, unordered messages with a maximum size (typically limited by the QUIC path MTU). Sent via
datagrams.write()and received viadatagrams.readable.
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:
- Ephemeral state updates (player positions, sensor readings, cursor movements): Use datagrams. Stale data is worse than lost data.
- Command and control messages (RPC calls, purchase confirmations, chat messages): Use bidirectional streams for reliable, ordered delivery.
- Large data pushes (file transfers, media segments, log dumps): Use unidirectional streams with backpressure-aware chunking.
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
- Validate all incoming data: Datagrams and stream data arrive as raw bytes. Never trust the client; sanitize and validate before processing.
- Rate limit streams: A malicious client could open thousands of streams. Enforce per-session limits using
maxStreamsBidirectionaland monitor stream creation rates. - Encrypt sensitive data end-to-end: While QUIC provides transport-layer encryption, consider application-layer encryption for highly sensitive payloads, especially if your server terminates TLS at a load balancer.
- Authenticate sessions: The initial HTTP/3 CONNECT request carries cookies and headers just like any HTTP request. Validate authentication tokens before accepting the Web Transport upgrade.
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);