# WebTransport API

> WebTransport() opens an HTTP/3 session with reliable streams and unreliable datagrams. Constructor options, each exception the spec defines, WebSocket fallback.

`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

```js
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 `ReadableStream`s of server-opened streams.

## 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

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](https://w3c.github.io/webtransport/#web-transport-error-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`. |

:::observed
Constructing `new WebTransport("http://localhost:4433/wt")` in Chrome throws `SyntaxError: Failed to construct 'WebTransport': The URL's scheme must be 'https'. 'http' is not allowed.`, and `new WebTransport("https://example.com/wt#live")` throws `SyntaxError: Failed to construct 'WebTransport': The URL contains a fragment identifier ('#live'). Fragment identifiers are not allowed in WebTransport URLs.` Passing `protocols: ["a", "a"]` throws `SyntaxError: Failed to construct 'WebTransport': Duplicate protocols are not allowed.` All three messages are in Chromium's [`web_transport.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webtransport/web_transport.cc) (chromium.googlesource.com).
:::

## 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

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.

```js
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

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`.

```js
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

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.

```js
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

- [WebRTC API](/reference/capabilities/webrtc/), whose `RTCDataChannel` is the peer-to-peer counterpart to WebTransport streams
- [WebCodecs](/reference/capabilities/webcodecs/), for encoding media frames before sending them over a WebTransport stream
- [WebTransport: constructor](https://w3c.github.io/webtransport/#webtransport-constructor) (w3.org)
- [WebTransport: WebTransportError interface](https://w3c.github.io/webtransport/#web-transport-error-interface) (w3.org)
- [WebTransport over HTTP/3](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3) (datatracker.ietf.org)
- [Using WebTransport](https://developer.chrome.com/docs/capabilities/web-apis/webtransport) (developer.chrome.com)