Capabilities · API
WebTransport API
Published Updated
WebTransport opens a client-to-server session over HTTP/3 (QUIC) and multiplexes two kinds of traffic on it: ordered, reliable byte streams, which the page reads and writes through WHATWG Streams, and unordered, unreliable datagrams for data where the newest message supersedes the last. It is the standards-track replacement for WebSocket when head-of-line blocking or TCP retransmission is the problem, and it is [SecureContext] and available in workers.
Chrome 97 shipped it on desktop, Android, and WebView, Edge 97 and Samsung Internet 18 followed, Firefox 114 added it, and Safari 26.4 brought it to macOS and iOS (BCD api.WebTransport). The congestionControl option is implemented in Firefox 114 and Safari 26.4 but not in Chrome, which accepts and ignores the member.
Syntax
Section titled “Syntax”new WebTransport(url)new WebTransport(url, options)
await transport.readytransport.closed
transport.createBidirectionalStream()transport.createBidirectionalStream(options)transport.createUnidirectionalStream()transport.createUnidirectionalStream(options)transport.incomingBidirectionalStreamstransport.incomingUnidirectionalStreamstransport.datagrams.readabletransport.datagrams.writable
transport.close()transport.close(closeInfo)The constructor returns synchronously and starts the connection; ready is a promise that fulfils once the session is established and closed is a promise that fulfils with a WebTransportCloseInfo on a clean close or rejects with a WebTransportError when the session fails. createBidirectionalStream() resolves with a WebTransportBidirectionalStream whose readable is a WebTransportReceiveStream and writable a WebTransportSendStream; createUnidirectionalStream() resolves with a WebTransportSendStream. The two incoming* attributes are ReadableStreams of server-opened streams.
Parameters
Section titled “Parameters”The constructor takes the server URL and an optional WebTransportOptions dictionary; close() takes an optional WebTransportCloseInfo. Every option has a default.
| Member | Type | Required | Description |
|---|---|---|---|
url |
USVString |
Yes | Absolute or relative URL of the HTTP/3 endpoint, resolved against the document’s API base URL. The scheme must be https and the URL may not carry a fragment. |
options.allowPooling |
boolean |
No | Default false. true lets the session share an existing HTTP/3 connection to the same origin instead of opening a dedicated one. |
options.requireUnreliable |
boolean |
No | Default false. true forbids falling back to HTTP/2 when the HTTP/3 handshake fails, so datagrams are guaranteed to be real QUIC datagrams. |
options.headers |
HeadersInit |
No | Default {}. Extra request headers for the CONNECT request that establishes the session. |
options.serverCertificateHashes |
sequence<WebTransportHash> |
No | Default []. { algorithm: "sha-256", value: BufferSource } entries; when non-empty the browser trusts the server only if a hash matches, which allows self-signed certificates on dedicated connections. Hashes with an unknown algorithm are ignored. |
options.congestionControl |
WebTransportCongestionControl |
No | "default", "throughput", or "low-latency"; a hint, downgraded to "default" when the browser has no matching algorithm. |
options.anticipatedConcurrentIncomingUnidirectionalStreams |
unsigned short? |
No | Default null. Tells the browser how many server-initiated unidirectional streams to expect so it can size flow-control limits. |
options.anticipatedConcurrentIncomingBidirectionalStreams |
unsigned short? |
No | Default null. Same for server-initiated bidirectional streams. |
options.protocols |
sequence<DOMString> |
No | Default []. Application protocol names offered to the server; the negotiated one is read back from transport.protocol. |
options.datagramsReadableType |
ReadableStreamType |
No | Set to "bytes" to make datagrams.readable a byte stream that supports BYOB readers. |
closeInfo.closeCode |
unsigned long |
No | Default 0. Application error code sent to the peer. |
closeInfo.reason |
USVString |
No | Default "". Human-readable close reason sent to the peer. |
Exceptions
Section titled “Exceptions”The constructor throws synchronously; the stream-creation methods return rejected promises. Session and stream failures surface as WebTransportError, a DOMException subclass with source ("session" or "stream") and streamErrorCode attributes (see the WebTransportError interface (w3.org)).
| Exception | Condition |
|---|---|
SyntaxError |
Thrown by the constructor when url fails to parse, its scheme is not https, it has a fragment, or protocols contains a duplicate, an empty name, or a name longer than 512 bytes. |
NotSupportedError |
Thrown by the constructor when allowPooling is true and serverCertificateHashes is non-empty; certificate pinning needs a dedicated connection. |
InvalidStateError |
createBidirectionalStream(), createUnidirectionalStream(), createSendGroup(), getStats(), and exportKeyingMaterial() reject or throw once the session is "closed" or "failed", including when it fails while the call is in flight. Setting sendGroup on a stream to a group from another transport throws it too. |
TypeError |
createBidirectionalStream() or createUnidirectionalStream() is given a sendGroup option that belongs to a different WebTransport. |
QuotaExceededError |
Stream IDs are exhausted and the call passed waitUntilAvailable: false; with the default true the promise waits instead. |
RangeError |
exportKeyingMaterial() is given a label or context longer than 255 bytes, or an output length of 0 or above the implementation limit (at least 4096). |
WebTransportError |
ready and closed reject with source: "session" when the CONNECT request fails or the handshake is refused; a reset stream’s readable and writable error with source: "stream" and the peer’s streamErrorCode. |
Examples
Section titled “Examples”Every example feature-detects the constructor and shows what the code does when the browser lacks it or the server is unreachable.
Connecting with a WebSocket fallback
Section titled “Connecting with a WebSocket fallback”Check for the global before constructing, then await ready inside a try, because a server without HTTP/3, a blocked UDP port, or a certificate problem rejects ready with a WebTransportError rather than throwing from the constructor.
async function connect(url) { if (typeof WebTransport === "undefined") { return { kind: "websocket", socket: new WebSocket(url.replace(/^https/, "wss")) }; } const transport = new WebTransport(url); try { await transport.ready; } catch (err) { console.warn(`WebTransport failed (${err.name}, source=${err.source}); using WebSocket`); return { kind: "websocket", socket: new WebSocket(url.replace(/^https/, "wss")) }; } transport.closed .then((info) => console.log(`closed: code ${info.closeCode} "${info.reason}"`)) .catch((err) => console.error(`session failed: ${err.message}`)); return { kind: "webtransport", transport };}The WebSocket path loses datagrams and per-stream independence, so the caller should treat kind as a capability flag rather than assuming identical behaviour.
Sending game state as datagrams
Section titled “Sending game state as datagrams”Datagrams are capped at transport.datagrams.maxDatagramSize bytes, a value the QUIC path MTU decides, and may be dropped or reordered, so each message must stand alone. Write with the datagram writer and read with a loop over datagrams.readable.
async function runDatagrams(transport, encodeState, applyState) { const writer = transport.datagrams.writable.getWriter(); const reader = transport.datagrams.readable.getReader(); const max = transport.datagrams.maxDatagramSize;
const timer = setInterval(async () => { const bytes = encodeState(); // typed array of the current state if (bytes.byteLength <= max) await writer.write(bytes); }, 50);
try { while (true) { const { value, done } = await reader.read(); if (done) break; applyState(value); } } finally { clearInterval(timer); writer.releaseLock(); }}When the session was pooled onto HTTP/2 because requireUnreliable was left false, datagrams are delivered reliably over the fallback, so latency behaviour differs; pass requireUnreliable: true when the unreliable semantics matter more than connectivity.
Echoing over a bidirectional stream and accepting server-opened streams
Section titled “Echoing over a bidirectional stream and accepting server-opened streams”A bidirectional stream is a pair of byte streams; pipe text through TextEncoderStream and TextDecoderStream so the application sees strings. Server-initiated streams arrive on incomingBidirectionalStreams and must be read in a loop or the server’s stream budget stalls.
async function echoOnce(transport, message) { let stream; try { stream = await transport.createBidirectionalStream(); } catch (err) { if (err.name === "InvalidStateError") return null; // session already closed throw err; } const writer = stream.writable.getWriter(); await writer.write(new TextEncoder().encode(message)); await writer.close();
const chunks = []; for await (const chunk of stream.readable.pipeThrough(new TextDecoderStream())) { chunks.push(chunk); } return chunks.join("");}
async function acceptServerStreams(transport, handle) { const reader = transport.incomingBidirectionalStreams.getReader(); while (true) { const { value: stream, done } = await reader.read(); if (done) break; handle(stream.readable, stream.writable); }}Closing the writer sends a FIN, which is how the echo server knows the request is complete; a stream left open holds its ID until the session ends.
See also
Section titled “See also”- WebRTC API, whose
RTCDataChannelis the peer-to-peer counterpart to WebTransport streams - WebCodecs, for encoding media frames before sending them over a WebTransport stream
- WebTransport: constructor (w3.org)
- WebTransport: WebTransportError interface (w3.org)
- WebTransport over HTTP/3 (datatracker.ietf.org)
- Using WebTransport (developer.chrome.com)
Specifications
| Specification | Status |
|---|---|
| WebTransport | W3C draft |
| WebTransport: constructor | W3C draft |
| WebTransport: createBidirectionalStream() | W3C draft |
| WebTransport: WebTransportError interface | W3C draft |
| WebTransport over HTTP/3 (IETF draft) | Other |
- Legend
- Yes
- Partial
- Flag
- No
- Unknown
| Browser / Platform | Support | Versions | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Desktop) | Yes | 97 | high | source | — |
| Chrome (Android) | Yes | 97 | high | source | 1 |
| Edge (Desktop) | Yes | 97 | high | source | 2 |
| Firefox (Desktop) | Yes | 114 | high | source | — |
| Firefox (Android) | Yes | 114 | high | source | 3 |
| Safari (macOS) | Yes | 26.4 | high | source | — |
| Safari (iOS) | Yes | 26.4 | high | source | 4 |
| Samsung Internet | Yes | 18.0 | high | source | 5 |
| WebView (Android) | Yes | 97 | high | source | 6 |
- Derived by browser-compat-data mirroring from Chrome.
- Derived by browser-compat-data mirroring from Chrome.
- Derived by browser-compat-data mirroring from Firefox.
- Derived by browser-compat-data mirroring from Safari.
- Derived by browser-compat-data mirroring from Chrome Android.
- Derived by browser-compat-data mirroring from Chrome Android.