Skip to main content

WebTransport

WebTransport provides ultra-low latency, bidirectional communication over QUIC/HTTP/3 using UDP.

Try it Online

Open WebTransport Client — See QUIC/HTTP3 streaming in action!

Experimental

WebTransport is disabled by default (WT_ENABLED=false). It requires specific infrastructure that supports QUIC/UDP.

Overview

PropertyValue
ProtocolUDP (QUIC/HTTP/3)
DirectionBidirectional
Latency0.5-2ms
StreamsMultiple (reliable + unreliable)
Browser SupportChrome 97+, Firefox 114+ (no Safari)

Why WebTransport?

WebTransport offers capabilities that WebSocket and SSE cannot:

FeatureWebSocketSSEWebTransport
Datagrams (unreliable)NoNoYes
Multiple streamsNoNoYes
0-RTT connectionNoNoYes
Connection migrationNoNoYes
Head-of-line blockingYesYesNo

Endpoint

https://datastream.hypetech.games:4433/wt
note

WebTransport requires HTTPS with a valid TLS certificate.

Configuration

Environment Variables

VariableDescriptionDefault
WT_ENABLEDEnable WebTransport serverfalse
WT_DOMAINDomain for Let's Encryptdatastream.hypetech.games
WT_PORTUDP/QUIC port4433
WT_EMAILEmail for Let's Encrypt-
CF_API_TOKENCloudflare token (DNS-01)-

Local Development

# Generate self-signed certificate
task certs:generate

# Add to macOS keychain
task certs:trust

# Start WebTransport server
task webtransport:run

Client Implementation

JavaScript (Browser)

class WebTransportClient {
constructor(url) {
this.url = url;
this.transport = null;
this.connected = false;
}

async connect() {
// Check browser support
if (typeof WebTransport === 'undefined') {
throw new Error('WebTransport not supported');
}

try {
this.transport = new WebTransport(this.url);
await this.transport.ready;
this.connected = true;
console.log('WebTransport connected');

// Handle connection close
this.transport.closed.then(() => {
this.connected = false;
console.log('WebTransport closed');
});

// Start reading datagrams
this.readDatagrams();

} catch (error) {
console.error('WebTransport error:', error);
throw error;
}
}

async readDatagrams() {
const reader = this.transport.datagrams.readable.getReader();

try {
while (true) {
const { value, done } = await reader.read();
if (done) break;

const text = new TextDecoder().decode(value);
const data = JSON.parse(text);
this.handleMessage(data);
}
} catch (error) {
console.error('Datagram reader error:', error);
}
}

handleMessage(data) {
console.log('Round:', data.round_id);
console.log('Result:', JSON.parse(data.extras));
}

async sendDatagram(data) {
if (!this.connected) return;

const writer = this.transport.datagrams.writable.getWriter();
const encoded = new TextEncoder().encode(JSON.stringify(data));
await writer.write(encoded);
writer.releaseLock();
}

disconnect() {
if (this.transport) {
this.transport.close();
}
}
}

// Usage
const client = new WebTransportClient('https://datastream.hypetech.games:4433/wt');
await client.connect();

React Hook

import { useState, useEffect, useCallback } from 'react';

function useWebTransport(url) {
const [data, setData] = useState(null);
const [connected, setConnected] = useState(false);
const [error, setError] = useState(null);
const [supported, setSupported] = useState(true);

useEffect(() => {
// Check support
if (typeof WebTransport === 'undefined') {
setSupported(false);
setError('WebTransport not supported in this browser');
return;
}

let transport;

async function connect() {
try {
transport = new WebTransport(url);
await transport.ready;
setConnected(true);

// Read datagrams
const reader = transport.datagrams.readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;

const text = new TextDecoder().decode(value);
setData(JSON.parse(text));
}
} catch (err) {
setError(err.message);
setConnected(false);
}
}

connect();

return () => {
if (transport) {
transport.close();
}
};
}, [url]);

return { data, connected, error, supported };
}

// Usage
function GameCard({ url }) {
const { data, connected, error, supported } = useWebTransport(url);

if (!supported) {
return <div>WebTransport not supported. Use Chrome or Firefox.</div>;
}

if (error) {
return <div>Error: {error}</div>;
}

if (!data) {
return <div>Connecting...</div>;
}

return (
<div>
<h2>{data.game_slug}</h2>
<p>Round: {data.round_id}</p>
<p>Latency: ~1ms</p>
</div>
);
}

Server Implementation

// internal/adapters/inbound/webtransport/server.go

type Server struct {
config Config
server *webtransport.Server
redis *redis.Client
}

func (s *Server) Start() error {
mux := http.NewServeMux()
mux.HandleFunc("/wt", s.handleWebTransport)

s.server = &webtransport.Server{
H3: http3.Server{
Addr: fmt.Sprintf(":%s", s.config.Port),
Handler: mux,
},
}

return s.server.ListenAndServeTLS(s.config.CertFile, s.config.KeyFile)
}

func (s *Server) handleWebTransport(w http.ResponseWriter, r *http.Request) {
session, err := s.server.Upgrade(w, r)
if err != nil {
return
}

ctx := r.Context()

// Subscribe to Redis
sub := s.redis.Subscribe(ctx, "stream:*")
defer sub.Close()

ch := sub.Channel()

for msg := range ch {
// Send as datagram (unreliable, low latency)
data := []byte(msg.Payload)
session.SendDatagram(data)
}
}

Infrastructure Requirements

Supported Platforms

PlatformStatusNotes
AWS EC2 + NLB✅ SupportedQUIC passthrough (Nov 2025)
Bare Metal/VPS✅ SupportedFull UDP control
Deno 2.2+⚠️ ExperimentalRequires --unstable-net
Fly.io⚠️ LimitedUDP works, QUIC unreliable
Cloudflare❌ Not SupportedNo WebTransport support
Railway/Render❌ Not SupportedNo UDP/QUIC

AWS NLB Configuration

# Create NLB with QUIC passthrough
aws elbv2 create-listener \
--load-balancer-arn $NLB_ARN \
--protocol TCP_QUIC \
--port 443 \
--default-actions Type=forward,TargetGroupArn=$TG_ARN

Fly.io Configuration

# fly.toml
[[services]]
internal_port = 4433
protocol = "udp"

[[services.ports]]
port = 4433

[env]
WT_ENABLED = "true"
WT_DOMAIN = "datastream.hypetech.games"
Fly.io Limitations
  • Requires dedicated IPv4 ($2/mo)
  • Must bind to fly-global-services
  • QUIC/HTTP/3 not officially supported

TLS Certificates

DNS-01 Challenge (Production)

WebTransport uses Let's Encrypt with DNS-01 challenge via Cloudflare:

solver := &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{
APIToken: os.Getenv("CF_API_TOKEN"),
},
}

Why DNS-01?

  • TLS-ALPN-01 doesn't work with QUIC
  • HTTP-01 requires port 80

Self-Signed (Development)

openssl req -x509 -newkey rsa:4096 \
-keyout certs/key.pem \
-out certs/cert.pem \
-days 365 -nodes \
-subj "/CN=localhost"

# Trust on macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain certs/cert.pem

Browser Support

BrowserVersionStatus
Chrome97+✅ Supported
Edge97+✅ Supported
Firefox114+✅ Supported
Safari-❌ Not Supported
iOS Safari-❌ Not Supported
caution

~50% of users don't have WebTransport support (Safari/iOS). Always implement WebSocket fallback.

Fallback Strategy

async function connectRealtime(game) {
// Try WebTransport first
if (typeof WebTransport !== 'undefined') {
try {
const wt = new WebTransportClient(WT_URL);
await wt.connect();
return wt;
} catch (e) {
console.log('WebTransport failed, falling back to WebSocket');
}
}

// Fallback to WebSocket
return new WebSocketClient(game);
}

Performance Comparison

MetricWebSocketSSEWebTransport
Connection time1 RTT1 RTT0-1 RTT
Message latency1-5ms10-50ms0.5-2ms
Overhead per message2-14 bytes~50 bytes2-6 bytes
Head-of-line blockingYesYesNo

Troubleshooting

"WebTransport not supported"

Cause: Browser doesn't support WebTransport

Solution: Use Chrome 97+ or Firefox 114+

"Opening handshake failed"

Causes:

  • Certificate not trusted
  • UDP port blocked
  • Server not reachable

Solutions:

  1. Add certificate to keychain
  2. Check firewall rules
  3. Verify server is running

"Connection closed unexpectedly"

Causes:

  • Network issues
  • Server restart
  • Idle timeout

Solutions:

  1. Implement reconnection logic
  2. Send keepalive datagrams
  3. Check server logs

When to Use WebTransport

Use WebTransport when:

  • Ultra-low latency is critical (< 2ms)
  • Unreliable delivery is acceptable
  • Users are on modern Chrome/Firefox
  • You control the infrastructure

Use WebSocket/SSE when:

  • Safari/iOS support is needed
  • Behind corporate proxies
  • Reliable delivery is required
  • Simple deployment is preferred