Learn Measure Blog Live About
Photo of fast-moving traffic.

Experimenting with WebTransport

Experimenting with WebTransport

WebTransport is a new 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.

Updated

Caution: This proposal has undergone significant changes since the start of the Origin Trial. Starting with Chrome 87, WebTransport has replaced QuicTransport as the top-level interface that developers interact with.

As a result, some of the information, and all of the sample code, in this article is out of date. For the latest on this evolving proposal, please read refer to the editor's draft of WebTransport. It includes an Examples section with updated code snippets.

Once the proposal stabilizes, we will update this article and associated code samples with up to date information.

Background

What's QuicTransport?

QuicTransport is a web API that uses the QUIC protocol in a bidirectional, non-HTTP transport. It's intended for two-way communications between a web client and a QUIC 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 QUIC streams is analogous to establishing multiple TCP connections, but QUIC streams are lightweight, and can be opened and closed without much overhead.

What's WebTransport?

QuicTransport is one part of the larger WebTransport proposal. WebTransport is a collection of APIs for sending and receiving data between a web client and a server. QuicTransport is the interface for using the QUIC protocol in the context of bi-directional WebTransport communications.

Chrome is implementing the QuicTransport portion of WebTransport first, before any of the other proposed interfaces. The Chrome team made the decision to start with a QuicTransport after speaking to web developers about their use cases. We hope to solicit early feedback on the overall WebTransport effort based on developers' experiences with QuicTransport.

Use cases

This a small list of possible ways developers might use QuicTransport.

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

As part of the origin trial process, we're interested in hearing more about how you plan on using QuicTransport!

Current status

Step Status
1. Create explainer Complete
2. Create initial draft of specification Complete
3. Gather feedback and iterate design In Progress
4. Origin trial In Progress
5. Launch Not Started

QuicTransport's relationship to other technologies

Is QuicTransport a replacement for WebSockets?

Maybe. There are use cases where either WebSockets or QuicTransport 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 QuicTransport's streams APIs can provide them as well. In comparison, QuicTransport's datagram APIs provide low-latency delivery, without guarantees about reliability or ordering, so they're not a direct replacement for WebSockets.

Using QuicTransport, 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 QUIC handshake is faster than starting up TCP over TLS.

QuicTransport 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 QuicTransport the same as a UDP Socket API?

No. QuicTransport is not a UDP Socket API. While QUIC does use UDP "under the hood," QuicTransport has requirements around encryption and congestion control that make it more than a basic UDP Socket API.

Is QuicTransport an alternative to WebRTC data channels?

Yes, for client-server connections. QuicTransport 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 QuicTransport only supports client-server connection. If you have multiple clients that need to talk directly to each other, then QuicTransport isn't a viable alternative.

Generally, running a QUIC-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 QuicTransport 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, QuicTransport is supported inside of Web Workers, which allows you to perform client-server communications independent of a given HTML page. Because QuicTransport 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 QuicTransport may not offer many advantages.

Try it out

The best way to experiment with QuicTransport is to use this Python code to start up a compatible QUIC server locally. You can then use this page with a basic JavaScript client to try out client/server communications.

Using the API

QuicTransport 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 QuicTransport origin trial supports three distinct types of traffic: datagrams, as well as both unidirectional and bidirectional streams.

Connecting to a server

You can connect to a QUIC server by creating a QuicTransport instance. The scheme of the URL should be quic-transport. 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 = 'quic-transport://example.com:4999/foo/bar';
const transport = new QuicTransport(url);

// Optionally, set up functions to respond to
// the connection closing:
transport.closed.then(() => {
console.log(`The QUIC connection to ${url} closed gracefully.`);
}).catch((error) => {
console.error('The QUIC connection to ${url} closed due to ${error}.');
});

// Once .ready fulfills, the connection can be used.
await transport.ready;

Datagram APIs

Once you have a QuicTransport instance that's connected to a server, you can use it to send and receive discrete bits of data, known as datagrams.

The sendDatagrams() method returns a WritableStream, which a web client can use to send data to the server. The receiveDatagrams() method 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 ws = transport.sendDatagrams();
const writer = ws.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 rs = transport.receiveDatagrams();
const reader = rs.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 QuicTransport 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.

SendStream

A SendStream is created by the web client using the createSendStream() method of a QuicTransport instance, which returns a promise for the SendStream.

Use the close() method of the WritableStreamDefaultWriter associated with the stream to send a QUIC Stream FIN bit to the server. The browser tries to send all pending data before actually closing the associated QUIC stream.

// Send two Uint8Arrays to the server.
const stream = await transport.createSendStream();
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 QUIC 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.createSendStream();
const writer = ws.getWriter();
writer.write(...);
writer.write(...);
await writer.abort();
// Not all the data may have been written.

ReceiveStream

A ReceiveStream is initiated by the server. Obtaining a ReceiveStream is a two-step process for a web client. First, it calls the receiveStreams() method of a QuicTransport instance, which returns a ReadableStream. Each chunk of that ReadableStream, is, in turn, a ReceiveStream 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.receiveStreams();
const reader = rs.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
// value is an instance of ReceiveStream
await readFrom(value);
}

You can detect stream closure using the closed promise of the ReadableStreamDefaultReader. When the QUIC stream is closed with the FIN bit, the closed promise is fulfilled after all the data is read. When the QUIC stream is closed abruptly (for example, by STREAM_RESET), 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.');
});

BidirectionalStream

A BidirectionalStream might be created either by the server or the client.

Web clients can create one using the createBidirectionalStream() method of a QuicTransport instance, which returns a promise for a BidirectionalStream.

const stream = await transport.createBidirectionalStream();
// stream is a BidirectionalStream
// stream.readable is a ReadableStream
// stream.writable is a WritableStream

You can listen for a BidirectionalStream created by the server with the receiveBidirectionalStreams() method of a QuicTransport instance, which returns a ReadableStream. Each chunk of that ReadableStream, is, in turn, a BidirectionalStream.

const rs = transport.receiveBidrectionalStreams();
const reader = rs.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
// value is a BidirectionalStream
// value.readable is a ReadableStream
// value.writable is a WritableStream
}

A BidirectionalStream is just a combination of a SendStream and ReceiveStream. 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.

Enabling support during the origin trial

  1. Request a token for your origin.
  2. Add the token to your pages. There are two ways to do that:
    • Add an origin-trial <meta> tag to the head of each page. For example, this may look something like:
      <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">
    • If you can configure your server, you can also add the token using an Origin-Trial HTTP header. The resulting response header should look something like:
      Origin-Trial: TOKEN_GOES_HERE

QuicTransport in Chrome's DevTools

Unfortunately, Chrome's DevTools support for QuicTransport is not ready for the start of the origin trial. Please "star" this Chrome issue to be notified about updates on the DevTools interface.

Privacy and security considerations

Please 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 throughout the origin trial process.

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.

  • Be sure you have signed up for the origin trial to show your interest and provide your domain and contact info.
  • Send a Tweet to @ChromiumDev with #QuicTransport 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.

Last updated: Improve article