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
| Property | Value |
|---|---|
| Protocol | UDP (QUIC/HTTP/3) |
| Direction | Bidirectional |
| Latency | 0.5-2ms |
| Streams | Multiple (reliable + unreliable) |
| Browser Support | Chrome 97+, Firefox 114+ (no Safari) |
Why WebTransport?
WebTransport offers capabilities that WebSocket and SSE cannot:
| Feature | WebSocket | SSE | WebTransport |
|---|---|---|---|
| Datagrams (unreliable) | No | No | Yes |
| Multiple streams | No | No | Yes |
| 0-RTT connection | No | No | Yes |
| Connection migration | No | No | Yes |
| Head-of-line blocking | Yes | Yes | No |
Endpoint
https://datastream.andrebassi.com.br:4433/wt
note
WebTransport requires HTTPS with a valid TLS certificate.
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
WT_ENABLED | Enable WebTransport server | false |
WT_DOMAIN | Domain for Let's Encrypt | datastream.andrebassi.com.br |
WT_PORT | UDP/QUIC port | 4433 |
WT_EMAIL | Email for Let's Encrypt | - |
CF_API_TOKEN | Cloudflare 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.andrebassi.com.br: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
| Platform | Status | Notes |
|---|---|---|
| AWS EC2 + NLB | ✅ Supported | QUIC passthrough (Nov 2025) |
| Bare Metal/VPS | ✅ Supported | Full UDP control |
| Deno 2.2+ | ⚠️ Experimental | Requires --unstable-net |
| Fly.io | ⚠️ Limited | UDP works, QUIC unreliable |
| Cloudflare | ❌ Not Supported | No WebTransport support |
| Railway/Render | ❌ Not Supported | No 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.andrebassi.com.br"
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
| Browser | Version | Status |
|---|---|---|
| Chrome | 97+ | ✅ Supported |
| Edge | 97+ | ✅ Supported |
| Firefox | 114+ | ✅ 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
| Metric | WebSocket | SSE | WebTransport |
|---|---|---|---|
| Connection time | 1 RTT | 1 RTT | 0-1 RTT |
| Message latency | 1-5ms | 10-50ms | 0.5-2ms |
| Overhead per message | 2-14 bytes | ~50 bytes | 2-6 bytes |
| Head-of-line blocking | Yes | Yes | No |
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:
- Add certificate to keychain
- Check firewall rules
- Verify server is running
"Connection closed unexpectedly"
Causes:
- Network issues
- Server restart
- Idle timeout
Solutions:
- Implement reconnection logic
- Send keepalive datagrams
- 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