Skip to content

Capabilities · API

WebTransport API

Published Updated

Newly available since 2026-03W3C draft

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.

new WebTransport(url)
new WebTransport(url, options)
await transport.ready
transport.closed
transport.createBidirectionalStream()
transport.createBidirectionalStream(options)
transport.createUnidirectionalStream()
transport.createUnidirectionalStream(options)
transport.incomingBidirectionalStreams
transport.incomingUnidirectionalStreams
transport.datagrams.readable
transport.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.

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.

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.

Every example feature-detects the constructor and shows what the code does when the browser lacks it or the server is unreachable.

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.

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.

Specifications

SpecificationStatus
WebTransportW3C draft
WebTransport: constructorW3C draft
WebTransport: createBidirectionalStream()W3C draft
WebTransport: WebTransportError interfaceW3C draft
WebTransport over HTTP/3 (IETF draft)Other
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Desktop)Yes97highsource—
Chrome (Android)Yes97highsource1
Edge (Desktop)Yes97highsource2
Firefox (Desktop)Yes114highsource—
Firefox (Android)Yes114highsource3
Safari (macOS)Yes26.4highsource—
Safari (iOS)Yes26.4highsource4
Samsung InternetYes18.0highsource5
WebView (Android)Yes97highsource6
  1. Derived by browser-compat-data mirroring from Chrome.
  2. Derived by browser-compat-data mirroring from Chrome.
  3. Derived by browser-compat-data mirroring from Firefox.
  4. Derived by browser-compat-data mirroring from Safari.
  5. Derived by browser-compat-data mirroring from Chrome Android.
  6. Derived by browser-compat-data mirroring from Chrome Android.

Source data: /compatibility/webtransport.json · Global usage: 93 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-10-03 · Confidence: high (computed from sources)