JavaScript WebTransport API Table
| Piece | What it does | Field note |
|---|---|---|
new WebTransport(url) | The handshake | HTTPS/3 mandatory; await wt.ready; self-signed certs only via serverCertificateHashes (SHA-256 pinning, 14-day cap) |
wt.datagrams | The lossy lane | roughly 1200-byte cap, unordered, unacknowledged, never retransmitted - a stale cursor is worse than a lost packet |
createBidirectionalStream() | The reliable lanes | independent ordered byte channels; server-initiated ones arrive on incomingBidirectional/UnidirectionalStreams |
the reliability split | The architecture | state ticks ride datagrams, commands and payloads ride streams - write the split into the protocol from day one |
no head-of-line blocking | The QUIC gift | streams multiplex independently: a stalled stream 3 does not delay stream 4 - the disease a TCP-backed WebSocket cannot cure |
congestionControl option | The dial | default vs low-latency at construction: quicker sends vs throughput; interactive picks low-latency, bulk transfer does not |
wt.close() / wt.closed | The exit | close({closeCode, reason}); closed resolves with close info; nothing auto-reconnects - wrap your own backoff and re-sync |
the support truth | The matrix | Chromium-only today (Chrome/Edge 97+); no polyfill can fake unordered unreliable delivery; UDP-blocked networks fail silently, keep the WebSocket ladder |
WebTransport is a client-server transport over HTTP/3 (QUIC) that finally gives the browser both halves of the network: reliable ordered streams AND unreliable unordered datagrams, multiplexed over one connection without head-of-line blocking. Where WebSockets give you a single reliable byte pipe (a dropped packet stalls everything behind it) and WebRTC data channels give you unreliable but with a peer-to-peer complication, WebTransport is client-server, low-latency, and honest about which messages can afford to be lost.
Bottom line: the architecture decision is the reliability split. Game state updates, cursor positions, live sensor ticks, telemetry - datagrams (transport.datagrams.writable / readable), where a stale packet is worse than a lost one and no retransmit clogs the line. Chat, file chunks, control commands - streams (transport.createBidirectionalStream() or the server-initiated variants), each independent, so one slow stream does not stall its siblings the way a TCP-backed WebSocket would. The connection handshake is HTTPS to a server that must present a certificate (self-signed allowed, pinned via serverCertificateHashes) - deployment is a server decision, not a CDN toggle.
The honest part: Chromium-only today (Chrome/Edge 97+, behind the maturity of the HTTP/3 ecosystem), no Safari or Firefox shipping it, and no polyfill can deliver what makes it valuable - unordered, unreliable, multiplexed delivery is a network-layer property. The fallback ladder is real, though: WebSockets cover the reliable half everywhere, and a feature-detect plus transport-choice layer means the same app protocol runs over both.
How to use
- Connect and wait: const wt = new WebTransport('https://server.example.com:4433/wt'); await wt.ready; - HTTPS mandatory, the server answers the HTTP/3 extended CONNECT; self-signed certs work only when you pass serverCertificateHashes (SHA-256 pinning).
- Send lossy and fast: const writer = wt.datagrams.writable.getWriter(); await writer.write(encoded); - datagrams are capped (~1200 bytes safe) unordered and unacknowledged; read them from wt.datagrams.readable with a reader loop.
- Open reliable lanes: const stream = await wt.createBidirectionalStream(); await stream.writable.getWriter().write(chunk); - each stream is independent; read the server's own streams from wt.incomingBidirectionalStreams / incomingUnidirectionalStreams.
- Choose per message type: telemetry and state ticks ride datagrams (drop stale, never queue), commands and payloads ride streams - write this split into the protocol from day one; retrofitting it is a rewrite.
- Close with intent: await wt.closed (a WebTransportCloseInfo) or call wt.close({closeCode, reason}) - for long-lived servers, watch session draining and re-connect logic (exponential backoff) because the API auto-reconnects nothing.
Frequently asked questions
When do I pick WebTransport over WebSockets - or over WebRTC?
Split by who owns the other end and what each message can afford. WebSockets: one reliable ordered pipe - perfect for chat, JSON APIs, anything where ordering matters and the volume is modest; the cost is head-of-line blocking, because one lost TCP packet stalls the whole pipe. WebRTC data channels: unreliable/unordered with configurable retransmission, but peer-to-peer machinery (signaling, STUN/TURN) you only want when peers talk to each other. WebTransport: client-to-server, low-latency, and both reliability modes at once - the fit is server-relayed realtime where state updates can drop (datagrams) while control traffic stays reliable (streams), all without head-of-line blocking between them. The pragmatic ladder: start with WebSockets if reliability alone suffices; reach for WebTransport when latency spikes under packet loss are measurably hurting you and you control the server; keep WebRTC for actual peer connections.
How do datagrams and streams actually differ in behavior?
Datagrams are fire-and-forget messages: capped at roughly one QUIC packet (~1200 bytes to be safe), not retransmitted, not ordered, not flow-controlled beyond a small send queue - write a stale cursor update and the network just skips it, which is the point. Streams are independent reliable byte channels: ordered within themselves, retransmitted on loss, flow-controlled, and multiplexed - a stalled upload on stream 3 does not delay stream 4 (this is the head-of-line-blocking fix vs a single TCP connection). The design consequence is protocol-level: tag each message with what it is (state vs command vs payload) and route it to the right lane; mixing everything into streams recreates a TCP-shaped app, while putting payloads in datagrams loses files. Backpressure exists on both: datagram writers reject when the queue is full (drop and move on - your loss budget), stream writers honor await write() as the pacing signal.
What is the certificate story for servers in development and production?
WebTransport requires HTTPS/3, but it deliberately supports non-public CAs: a development server with a self-signed certificate works if the client pins it - generate the cert, hash it (SHA-256), and pass serverCertificateHashes: [{algorithm: 'SHA-256', value: hash}] in the WebTransport constructor; the browser then accepts exactly that cert for exactly that session. In production, a normal publicly-trusted certificate makes the hashes unnecessary (and hashes are capped at 14 days validity by spec - they are a dev bootstrap, not a PKI replacement). The deployment prerequisites that actually bite: the server must speak HTTP/3 (QUIC) and answer the extended CONNECT upgrade - frameworks like quic-go, aioquic and echo servers for Rust/Go provide it; UDP must not be firewalled on the path (corporate networks block it, which is why your fallback transport matters); and CDNs are only beginning to offer WebTransport pass-through, so self-hosting is currently the norm.
How should reconnection and backpressure be designed?
Assume the session dies and design the join-back as a first-class flow. The API gives you wt.closed (a promise resolving with close info or rejecting on failure) and nothing automatic: no reconnect, no resumption token exposed to apps - so wrap the transport in a connection manager with exponential backoff and jitter, a fresh WebTransport per attempt, and a re-sync step after ready (a 'since' cursor or snapshot request) because datagrams you missed are gone forever. Backpressure discipline: for datagrams, treat writer.write() rejection or a full queue as your signal to drop stale messages (keep only the newest state per key); for streams, await each write and let the natural pacing push back on producers; and watch transport.congestionControl ('default' vs 'low-latency' at construction) - the low-latency option trades throughput for quicker sends, right for interactive apps, wrong for bulk transfer. Above all, version the handshake: the first reliable message on connect should declare protocol version and capability flags, so old clients and new servers negotiate instead of corrupting each other.