Using WebTransport
WebTransport is an API offering low-latency, bidirectional, client-server messaging. Learn more about its use cases, and how to give feedback about the future of the implementation.
Background
What's WebTransport?
WebTransport is a web API that uses the HTTP/3 protocol as a bidirectional transport. It's intended for two-way communications between a web client and an HTTP/3 server. It supports sending data both unreliably via its datagram APIs, and reliably via its streams APIs.
Datagrams are ideal for sending and receiving data that do not need strong delivery guarantees. Individual packets of data are limited in size by the maximum transmission unit (MTU) of the underlying connection, and may or may not be transmitted successfully, and if they are transferred, they may arrive in an arbitrary order. These characteristics make the datagram APIs ideal for low-latency, best-effort data transmission. You can think of datagrams as user datagram protocol (UDP) messages, but encrypted and congestion-controlled.
The streams APIs, in contrast, provide reliable, ordered data transfer. They're well-suited to scenarios where you need to send or receive one or more streams of ordered data. Using multiple WebTransport streams is analogous to establishing multiple TCP connections, but since HTTP/3 uses the lighter-weight QUIC protocol under the hood, they can be opened and closed without as much overhead.
Use cases
This a small list of possible ways developers might use WebTransport.
- Sending game state at a regular interval with minimal latency to a server via small, unreliable, out-of-order messages.
- Receiving media streams pushed from a server with minimal latency, independent of other data streams.
- Receiving notifications pushed from a server while a web page is open.
We're interested in hearing more about how you plan to use WebTransport.
Many of the concepts in this proposal were previously experimented with as part of the earlier QuicTransport origin trial, which did not end up being released as part of Chrome.
WebTransport helps with similar use cases as QuicTransport, with the primary difference being that HTTP/3 instead of QUIC is the underlying transport protocol.
Browser support
- chrome 97, Supported 97
- firefox, Not supported ×
- edge 97, Supported 97
- safari, Not supported ×
As with all features that do not have universal browser support, coding defensively via feature detection is a best practice.
Current status
WebTransport's relationship to other technologies
Is WebTransport a replacement for WebSockets?
Maybe. There are use cases where either WebSockets or WebTransport might be valid communication protocols to use.
WebSockets communications are modeled around a single, reliable, ordered stream of messages, which is fine for some types of communication needs. If you need those characteristics, then WebTransport's streams APIs can provide them as well. In comparison, WebTransport's datagram APIs provide low-latency delivery, without guarantees about reliability or ordering, so they're not a direct replacement for WebSockets.
Using WebTransport, via the datagram APIs or via multiple concurrent Streams API instances, means that you don't have to worry about head-of-line blocking, which can be an issue with WebSockets. Additionally, there are performance benefits when establishing new connections, as the underlying QUIC handshake is faster than starting up TCP over TLS.
WebTransport is part of a new draft specification, and as such the WebSocket ecosystem around client and server libraries is currently much more robust. If you need something that works "out of the box" with common server setups, and with broad web client support, WebSockets is a better choice today.
Is WebTransport the same as a UDP Socket API?
No. WebTransport is not a UDP Socket API. While WebTransport uses HTTP/3, which in turn uses UDP "under the hood," WebTransport has requirements around encryption and congestion control that make it more than a basic UDP Socket API.
Is WebTransport an alternative to WebRTC data channels?
Yes, for client-server connections. WebTransport shares many of the same properties as WebRTC data channels, although the underlying protocols are different.
WebRTC data channels support peer-to-peer communications, but WebTransport only supports client-server connection. If you have multiple clients that need to talk directly to each other, then WebTransport isn't a viable alternative.
Generally, running a HTTP/3-compatible server requires less setup and configuration than maintaining a WebRTC server, which involves understanding multiple protocols (ICE, DTLS, and SCTP) in order to get a working transport. WebRTC entails many more moving pieces that could lead to failed client/server negotiations.
The WebTransport API was designed with the web developer use cases in mind, and should feel more like writing modern web platform code than using WebRTC's data channel interfaces. Unlike WebRTC, WebTransport is supported inside of Web Workers, which allows you to perform client-server communications independent of a given HTML page. Because WebTransport exposes a Streams-compliant interface, it supports optimizations around backpressure.
However, if you already have a working WebRTC client/server setup that you're happy with, switching to WebTransport may not offer many advantages.
Try it out
The best way to experiment with WebTransport is to start up a compatible HTTP/3 server. You can then use this page with a basic JavaScript client to try out client/server communications.
Additionally, a community-maintained echo server is available at webtransport.day.
Using the API
WebTransport was designed on top of modern web platform primitives, like the Streams API. It relies heavily on promises, and works well with async
and await
.
The current WebTransport implementation in Chromium supports three distinct types of traffic: datagrams, as well as both unidirectional and bidirectional streams.
Connecting to a server
You can connect to a HTTP/3 server by creating a WebTransport
instance. The scheme of the URL should be https
. You need to explicitly specify the port number.
You should use the ready
promise to wait for the connection to be established. This promise will not be fulfilled until the setup is complete, and will reject if the connection fails at the QUIC/TLS stage.
The closed
promise fulfills when the connection closes normally, and rejects if the closure was unexpected.
If the server rejects the connection due to a client indication error (e.g. the path of the URL is invalid), then that causes closed
to reject, while ready
remains unresolved.
const url = 'https://example.com:4999/foo/bar';
const transport = new WebTransport(url);
// Optionally, set up functions to respond to
// the connection closing:
transport.closed.then(() => {
console.log(`The HTTP/3 connection to ${url} closed gracefully.`);
}).catch((error) => {
console.error(`The HTTP/3 connection to ${url} closed due to ${error}.`);
});
// Once .ready fulfills, the connection can be used.
await transport.ready;
Datagram APIs
Once you have a WebTransport instance that's connected to a server, you can use it to send and receive discrete bits of data, known as datagrams.
The writeable
getter returns a WritableStream
, which a web client can use to send data to the server. The readable
getter returns a ReadableStream
, allowing you to listen for data from the server. Both streams are inherently unreliable, so it is possible that the data you write will not be received by the server, and vice versa.
Both types of streams use Uint8Array
instances for data transfer.
// Send two datagrams to the server.
const writer = transport.datagrams.writable.getWriter();
const data1 = new Uint8Array([65, 66, 67]);
const data2 = new Uint8Array([68, 69, 70]);
writer.write(data1);
writer.write(data2);
// Read datagrams from the server.
const reader = transport.datagrams.readable.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) {
break;
}
// value is a Uint8Array.
console.log(value);
}
Chrome does not currently expose an async iterator for a ReadableStream
. For the time being, using the getReader()
method combined with a while()
loop is the best way to read from the stream.
Streams APIs
Once you've connected to the server, you could also use WebTransport to send and receive data via its Streams APIs.
Each chunk of all streams is a Uint8Array
. Unlike with the Datagram APIs, these streams are reliable. But each stream is independent, so data order across streams is not guaranteed.
WebTransportSendStream
A WebTransportSendStream
is created by the web client using the createUnidirectionalStream()
method of a WebTransport
instance, which returns a promise for the WebTransportSendStream
.
Use the close()
method of the WritableStreamDefaultWriter
to close the associated HTTP/3 connection. The browser tries to send all pending data before actually closing the associated connection.
// Send two Uint8Arrays to the server.
const stream = await transport.createUnidirectionalStream();
const writer = stream.writable.getWriter();
const data1 = new Uint8Array([65, 66, 67]);
const data2 = new Uint8Array([68, 69, 70]);
writer.write(data1);
writer.write(data2);
try {
await writer.close();
console.log('All data has been sent.');
} catch (error) {
console.error(`An error occurred: ${error}`);
}
Similarly, use the abort()
method of the WritableStreamDefaultWriter
to send a RESET_STREAM
to the server. When using abort()
, the browser may discard any pending data that hasn't yet been sent.
const ws = await transport.createUnidirectionalStream();
const writer = ws.getWriter();
writer.write(...);
writer.write(...);
await writer.abort();
// Not all the data may have been written.
WebTransportReceiveStream
A WebTransportReceiveStream
is initiated by the server. Obtaining a WebTransportReceiveStream
is a two-step process for a web client. First, it calls the incomingUnidirectionalStreams
attribute of a WebTransport
instance, which returns a ReadableStream
. Each chunk of that ReadableStream
, is, in turn, a WebTransportReceiveStream
that can be used to read Uint8Array
instances sent by the server.
async function readFrom(receiveStream) {
const reader = receiveStream.readable.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
// value is a Uint8Array
console.log(value);
}
}
const rs = transport.incomingUnidirectionalStreams;
const reader = rs.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
// value is an instance of WebTransportReceiveStream
await readFrom(value);
}
You can detect stream closure using the closed
promise of the ReadableStreamDefaultReader
. When the underlying HTTP/3 connection is closed with the FIN bit, the closed
promise is fulfilled after all the data is read. When the HTTP/3 connection is closed abruptly (for example, by RESET_STREAM
), then the closed
promise rejects.
// Assume an active receiveStream
const reader = receiveStream.readable.getReader();
reader.closed.then(() => {
console.log('The receiveStream closed gracefully.');
}).catch(() => {
console.error('The receiveStream closed abruptly.');
});
WebTransportBidirectionalStream
A WebTransportBidirectionalStream
might be created either by the server or the client.
Web clients can create one using the createBidirectionalStream()
method of a WebTransport
instance, which returns a promise for a WebTransportBidirectionalStream
.
const stream = await transport.createBidirectionalStream();
// stream is a WebTransportBidirectionalStream
// stream.readable is a ReadableStream
// stream.writable is a WritableStream
You can listen for a WebTransportBidirectionalStream
created by the server with the incomingBidirectionalStreams
attribute of a WebTransport
instance, which returns a ReadableStream
. Each chunk of that ReadableStream
, is, in turn, a WebTransportBidirectionalStream
.
const rs = transport.incomingBidirectionalStreams;
const reader = rs.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
// value is a WebTransportBidirectionalStream
// value.readable is a ReadableStream
// value.writable is a WritableStream
}
A WebTransportBidirectionalStream
is just a combination of a WebTransportSendStream
and WebTransportReceiveStream
. The examples from the previous two sections explain how to use each of them.
More examples
The WebTransport draft specification includes a number of additional inline examples, along with full documentation for all of the methods and properties.
WebTransport in Chrome's DevTools
Unfortunately, Chrome's DevTools do not currently support WebTransport. You can "star" this Chrome issue to be notified about updates on the DevTools interface.
Polyfill
A polyfill (or rather ponyfill that provides functionality as a standalone module you can use) called webtransport-ponyfill-websocket
that implements some of the features of WebTransport is available. Carefully read the constraints in the project's README
to determine if this solution can work for your use case.
Privacy and security considerations
See the corresponding section of the draft specification for authoritative guidance.
Feedback
The Chrome team wants to hear your thoughts and experiences using this API.
Feedback about the API design
Is there something about the API that's awkward or doesn't work as expected? Or are there missing pieces that you need to implement your idea?
File an issue on the Web Transport GitHub repo, or add your thoughts to an existing issue.
Problem with the implementation?
Did you find a bug with Chrome's implementation?
File a bug at https://new.crbug.com. Include as much detail as you can, along with simple instructions for reproducing.
Planning to use the API?
Your public support helps Chrome prioritize features, and shows other browser vendors how critical it is to support them.
- Send a tweet to @ChromiumDev using the hashtag
#WebTransport
and details on where and how you're using it.
General discussion
You can use the web-transport-dev Google Group for general questions or problems that don't fit into one of the other categories.
Acknowledgements
This article incorporates information from the WebTransport Explainer, draft specification, and related design docs. Thank you to the respective authors for providing that foundation.
The hero image on this post is by Robin Pierre on Unsplash.