Guide to the WebTransport API
16 January 202516 min read
Discover the WebTransport API, a revolutionary technology that enables efficient, low-latency communication between web clients and servers. Learn how this innovative protocol enhances real-time data transfer, supports bidirectional streams, and improves overall web application performance. Explore its use cases, benefits, and implementation tips to stay ahead in modern web development.
The WebTransport API
offers a modern alternative to WebSockets, facilitating data transmission between clients and servers using the HTTP/3 Transport. It supports multiple streams, unidirectional streams, and out-of-order delivery. WebTransport enables reliable communication through streams and unreliable transport via UDP-like datagrams.
Definition and Features
The WebTransport API
is an interface for transmitting data between clients and servers using the HTTP/3 protocol.
It supports reliable, ordered delivery of data through one or more uni- or bidirectional streams, as well as unreliable, unordered delivery via datagrams. In the former, it serves as an alternative to WebSockets, and in the latter, it acts as an alternative to RTCDataChannel provided by the WebRTC API.

As of now, neither Node.js, Deno, nor Bun support the WebTransport API
.
In June 2023, WebTransport support was added to Socket.io. However, this support relies on the package @fails-components/webtransport, which appears more like an experimental tool not intended for production use. Despite this, let's take a closer look at this option, as Socket.io is a well-established library known for real-time data exchange.
HTTP/3 Overview
HTTP/3 is built on Google’s QUIC protocol, which itself is based on UDP and aims to address several limitations inherent to the TCP protocol:
- Head-of-Line (HOL) Blocking: Unlike HTTP/2, which supports multiplexing (multiple streams transmitted over a single connection), if one stream fails in HTTP/2, others must wait for it to be restored. QUIC eliminates this issue by allowing streams to be independent.
- Higher Performance: QUIC outperforms TCP in various ways. One key reason is that QUIC natively implements security measures (unlike TCP, which relies on TLS), leading to fewer round trips. Additionally, QUIC streams offer a more efficient transport mechanism compared to TCP's packet-based transmission, particularly beneficial in heavily loaded networks.
- Seamless Network Transition: QUIC uses a unique connection identifier that allows packets to be delivered correctly across different networks. This identifier can persist across network changes, enabling uninterrupted downloads even when switching from Wi-Fi to mobile networks. In contrast, HTTP/2 relies on IP addresses, which can cause disruptions during network transitions.
- Unreliable Delivery: HTTP/3 supports unreliable delivery, which can be more efficient than guaranteed delivery in certain scenarios.
To enable HTTP/3 (QUIC) support in Google Chrome, go to chrome://flags
and enable the Experimental QUIC protocol option.

Working Principles of WebTransport API
Establishing a Connection
To establish an HTTP/3 connection with a server, the URL must be passed to the WebTransport()
constructor. Note that the URL should use the HTTPS scheme, and the port must be specified explicitly. The WebTransport.ready
promise resolves once the connection is successfully established.
The connection can be closed using the WebTransport.closed
promise. Any errors encountered are instances of WebTransportError
, which include additional details on top of the standard DOMException
set.
1 const url = "https://example.com:4999/wt";23 async function initTransport(url) {4 // Initialize the connection5 const transport = new WebTransport(url);67 // Resolving this promise indicates readiness to handle requests8 await transport.ready;910 // ...11 }1213 async function closeTransport(transport) {14 // Handle connection closing15 try {16 await transport.closed;17 console.log(`HTTP/3 connection to ${url} closed gracefully.`);18 } catch (error) {19 console.error(`HTTP/3 connection to ${url} closed due to error: ${error}.`);20 }21 }
This setup ensures you can effectively manage connections and handle errors when working with the WebTransport API over HTTP/3.
Unreliable Data Transmission Using Datagrams
Unreliable transmission means that there is no guarantee of complete data delivery or the order in which it arrives. In some cases, this is acceptable, with the main advantage being faster data transfer speeds.
Unreliable data delivery is handled through the WebTransport.datagrams property, which returns a WebTransportDatagramDuplexStream object containing everything necessary for sending datagrams to the server and receiving them on the client.
The WebTransportDatagramDuplexStream.writable property provides a WritableStream object, allowing data to be sent to the server:
1 const writer = transport.datagrams.writable.getWriter();2 const data1 = new Uint8Array([65, 66, 67]);3 const data2 = new Uint8Array([68, 69, 70]);4 writer.write(data1);5 writer.write(data2);
The WebTransportDatagramDuplexStream.readable property provides a ReadableStream, allowing data received from the server to be read:
1 async function readData() {2 const reader = transport.datagrams.readable.getReader();34 while (true) {5 const { value, done } = await reader.read();67 if (done) {8 break;9 }1011 console.log(value); // value is a Uint8Array12 }13 }
Reliable Data Transmission Using Streams
Reliable transmission ensures complete and ordered data delivery, though it takes longer compared to datagrams. However, reliability is critical in many scenarios, such as chat applications.
When using streams for data transmission, it’s possible to define stream priorities.
Unidirectional Data Transmission
To open a unidirectional stream, use the WebTransport.createUnidirectionalStream() method, which returns a WritableStream
. Data is sent to the server using the writer returned by getWriter
:
1 async function writeData() {2 const stream = await transport.createUnidirectionalStream();3 const writer = stream.writable.getWriter();4 const data1 = new Uint8Array([65, 66, 67]);5 const data2 = new Uint8Array([68, 69, 70]);6 writer.write(data1);7 writer.write(data2);89 try {10 await writer.close();11 console.log("All data has been successfully sent");12 } catch (error) {13 console.error(`An error occurred while sending data: ${error}`);14 }15 }
The WritableStreamDefaultWriter.close()
method is used to close the HTTP/3 connection after sending all data.
To extract data from a unidirectional stream opened on the server, use the WebTransport.incomingUnidirectionalStreams
property, which returns ReadableStream
objects of WebTransportReceiveStream
.
Create a function to read from WebTransportReceiveStream
. These objects inherit from the ReadableStream
class, making it easy to implement:
1 async function readData(receiveStream) {2 const reader = receiveStream.getReader();34 while (true) {5 const { done, value } = await reader.read();67 if (done) {8 break;9 }1011 console.log(value); // value is a Uint8Array12 }13 }
Get a reference to reader using getReader()
and read from incomingUnidirectionalStreams
in chunks (each chunk is a WebTransportReceiveStream
):
1 async function receiveUnidirectional() {2 const uds = transport.incomingUnidirectionalStreams;3 const reader = uds.getReader();45 while (true) {6 const { done, value } = await reader.read();78 if (done) {9 break;10 }1112 await readData(value);13 }14 }
These mechanisms allow effective handling of both reliable and unreliable data transmissions using the WebTransport API.
Bidirectional Data Transmission
To open a bidirectional stream, use the WebTransport.createBidirectionalStream() method, which returns a WebTransportBidirectionalStream. This stream contains readable and writable properties that provide references to instances of WebTransportReceiveStream
and WebTransportSendStream
. These can be used for reading data received from the server and sending data to the server, respectively.
1 async function setUpBidirectional() {2 const stream = await transport.createBidirectionalStream();3 // stream is WebTransportBidirectionalStream4 // stream.readable is WebTransportReceiveStream5 const readable = stream.readable;6 // stream.writable is WebTransportSendStream7 const writable = stream.writable;89 // Additional setup code can follow10 }
Reading from WebTransportReceiveStream
can be implemented as follows:
1 async function readData(readable) {2 const reader = readable.getReader();34 while (true) {5 const { value, done } = await reader.read();67 if (done) {8 break;9 }1011 console.log(value); // value is Uint8Array12 }13 }
Writing to WebTransportSendStream
can be implemented as follows:
1 async function writeData(writable) {2 const writer = writable.getWriter();3 const data1 = new Uint8Array([65, 66, 67]);4 const data2 = new Uint8Array([68, 69, 70]);5 writer.write(data1);6 writer.write(data2);7 }
To extract data from a bidirectional stream opened on the server, use the WebTransport.incomingBidirectionalStreams property, which returns ReadableStream
objects of WebTransportBidirectionalStream. Each stream can be used for reading and writing instances of Uint8Array
. You’ll need a function to handle reading from the bidirectional stream:
1 async function receiveBidirectional() {2 const bds = transport.incomingBidirectionalStreams;34 const reader = bds.getReader();56 while (true) {7 const { done, value } = await reader.read();89 if (done) {10 break;11 }1213 await readData(value.readable);14 await writeData(value.writable);15 }16 }
This approach allows the handling of bidirectional data streams efficiently, making full use of the WebTransport API.
How to Implement WebTransport API using JS?
1 // Create a WebTransport connection2 class TransportClient {3 constructor(url) {4 this.url = url;5 this.transport = null;6 this.streams = new Map();7 this.datagramWriter = null;8 this.datagramReader = null;9 }1011 async connect() {12 try {13 this.transport = new WebTransport(this.url);14 console.log("Initiating connection...");1516 // Wait for connection establishment17 await this.transport.ready;18 console.log("Connection established successfully");1920 // Set up error handling21 this.transport.closed22 .then(() => {23 console.log("Connection closed normally");24 })25 .catch((error) => {26 console.error("Connection closed due to error:", error);27 });2829 // Initialize datagram handlers30 this.setupDatagrams();31 } catch (error) {32 console.error("Failed to establish connection:", error);33 throw error;34 }35 }3637 // Set up datagram sending and receiving38 setupDatagrams() {39 // Set up datagram writer40 this.datagramWriter = this.transport.datagrams.writable.getWriter();4142 // Set up datagram reader43 this.handleDatagrams();44 }4546 async handleDatagrams() {47 try {48 const reader = this.transport.datagrams.readable.getReader();49 while (true) {50 const { value, done } = await reader.read();51 if (done) {52 console.log("Datagram reader done");53 break;54 }55 // Process received datagram56 const decoded = new TextDecoder().decode(value);57 console.log("Received datagram:", decoded);58 }59 } catch (error) {60 console.error("Error reading datagrams:", error);61 }62 }6364 // Send a datagram65 async sendDatagram(data) {66 try {67 const encoded = new TextEncoder().encode(data);68 await this.datagramWriter.write(encoded);69 console.log("Datagram sent successfully");70 } catch (error) {71 console.error("Error sending datagram:", error);72 throw error;73 }74 }7576 // Create and handle a bidirectional stream77 async createBidirectionalStream() {78 try {79 const stream = await this.transport.createBidirectionalStream();80 const streamId = crypto.randomUUID();81 this.streams.set(streamId, stream);8283 // Handle incoming data84 this.handleStreamInput(stream, streamId);8586 return {87 streamId,88 writer: stream.writable.getWriter(),89 };90 } catch (error) {91 console.error("Error creating bidirectional stream:", error);92 throw error;93 }94 }9596 async handleStreamInput(stream, streamId) {97 try {98 const reader = stream.readable.getReader();99 while (true) {100 const { value, done } = await reader.read();101 if (done) {102 console.log(`Stream ${streamId} reading complete`);103 break;104 }105 const decoded = new TextDecoder().decode(value);106 console.log(`Received on stream ${streamId}:`, decoded);107 }108 } catch (error) {109 console.error(`Error reading from stream ${streamId}:`, error);110 } finally {111 this.streams.delete(streamId);112 }113 }114115 // Send data through a specific stream116 async sendOnStream(streamId, data) {117 const stream = this.streams.get(streamId);118 if (!stream) {119 throw new Error(`Stream ${streamId} not found`);120 }121122 try {123 const writer = stream.writable.getWriter();124 const encoded = new TextEncoder().encode(data);125 await writer.write(encoded);126 await writer.close();127 console.log(`Data sent successfully on stream ${streamId}`);128 } catch (error) {129 console.error(`Error sending data on stream ${streamId}:`, error);130 throw error;131 }132 }133134 // Close the WebTransport connection135 async close() {136 try {137 await this.transport.close();138 console.log("Connection closed successfully");139 } catch (error) {140 console.error("Error closing connection:", error);141 throw error;142 }143 }144 }
Here's how to use the WebTransport implementation:
1 // Usage example2 async function main() {3 // Create a new transport client4 const client = new TransportClient("https://example.com/webtransport");56 try {7 // Connect to the server8 await client.connect();910 // Send a datagram11 await client.sendDatagram("Hello via datagram!");1213 // Create a bidirectional stream14 const { streamId, writer } = await client.createBidirectionalStream();1516 // Send data through the stream17 await client.sendOnStream(streamId, "Hello via stream!");1819 // Close the connection when done20 await client.close();21 } catch (error) {22 console.error("Error:", error);23 }24 }
WebTransport Real-time Communication Demo
1 // server.js2 import { WebTransport } from "@fails-components/webtransport";3 import { createServer } from "http";45 const server = createServer();6 const port = 8080;78 // Create WebTransport server instance9 const wtServer = new WebTransport({10 port: port,11 host: "localhost",12 certificates: [], // Add your SSL certificates for production13 });1415 wtServer.on("session", async (session) => {16 console.log("New WebTransport session established");1718 // Handle bidirectional streams19 session.on("stream", async (stream) => {20 const reader = stream.readable.getReader();21 const writer = stream.writable.getWriter();2223 try {24 while (true) {25 const { value, done } = await reader.read();26 if (done) break;2728 // Echo received data back to client29 const response = `Server received: ${new TextDecoder().decode(value)}`;30 await writer.write(new TextEncoder().encode(response));31 }32 } catch (err) {33 console.error("Stream error:", err);34 } finally {35 reader.releaseLock();36 writer.releaseLock();37 }38 });39 });4041 server.listen(port, () => {42 console.log(`Server listening on port ${port}`);43 });4445 // client.js46 class WebTransportClient {47 constructor() {48 this.transport = null;49 this.stream = null;50 }5152 async connect() {53 try {54 this.transport = new WebTransport("https://localhost:8080/webtransport");55 await this.transport.ready;56 console.log("WebTransport connection established");5758 // Handle connection close59 this.transport.closed60 .then(() => console.log("Connection closed normally"))61 .catch((error) =>62 console.error("Connection closed with error:", error)63 );64 } catch (err) {65 console.error("Failed to establish WebTransport connection:", err);66 }67 }6869 async createStream() {70 try {71 this.stream = await this.transport.createBidirectionalStream();72 console.log("Bidirectional stream created");7374 // Set up stream reader75 this.startReading();76 return this.stream;77 } catch (err) {78 console.error("Failed to create stream:", err);79 }80 }8182 async startReading() {83 const reader = this.stream.readable.getReader();8485 try {86 while (true) {87 const { value, done } = await reader.read();88 if (done) break;8990 console.log("Received:", new TextDecoder().decode(value));91 }92 } catch (err) {93 console.error("Error reading from stream:", err);94 } finally {95 reader.releaseLock();96 }97 }9899 async sendMessage(message) {100 if (!this.stream) {101 console.error("No active stream");102 return;103 }104105 const writer = this.stream.writable.getWriter();106 try {107 await writer.write(new TextEncoder().encode(message));108 console.log("Message sent:", message);109 } catch (err) {110 console.error("Error sending message:", err);111 } finally {112 writer.releaseLock();113 }114 }115116 async close() {117 if (this.transport) {118 await this.transport.close();119 console.log("Connection closed");120 }121 }122 }123124 // Usage example125 async function main() {126 const client = new WebTransportClient();127128 // Connect to server129 await client.connect();130131 // Create bidirectional stream132 await client.createStream();133134 // Send test message135 await client.sendMessage("Hello WebTransport!");136137 // Close connection after 5 seconds138 setTimeout(async () => {139 await client.close();140 }, 5000);141 }142143 main().catch(console.error);
WebTransport Demo With Socket.Io
Here's a demo app showcasing real-time communication using the WebTransport API.
First, create a new directory, navigate into it, and initialize a Node.js project:
1 mkdir webtransport-socket-example2 cd webtransport-socket-example3 npm init -yp
Note: WebTransport can only function in a secure context (HTTPS), so even localhost isn't an exception. We need to generate an SSL certificate and key.
openssl req -newkey rsa:2048 -keyout PRIVATEKEY.key -out MYCSR.csr
Read more about certificates - https://www.ssl.com/how-to/manually-generate-a-certificate-signing-request-csr-using-openssl/
Next, let's install a few packages:
npm i express socket.io @fails-components/webtransport
Now, define the server code and its startup script in the package.json
file:
1 "main": "server.js",2 "scripts": {3 "start": "nodemon"4 },5 "type": "module",
Create a file server.js
with the following content:
1 import express from "express";2 import { readFileSync } from "node:fs";3 import { createServer } from "node:https";4 import path from "node:path";56 // Read SSL key and certificate7 const key = readFileSync("./key.pem");8 const cert = readFileSync("./cert.pem");910 // Create the Express app11 const app = express();12 // Serve `index.html` for all requests13 app.use("*", (req, res) => {14 res.sendFile(path.resolve("./index.html"));15 });1617 // Create the HTTPS server18 const httpsServer = createServer({ key, cert }, app);1920 const port = process.env.PORT || 443;2122 // Start the server23 httpsServer.listen(port, () => {24 console.log(`Server listening at https://localhost:${port}`);25 });
Create a file index.html
with the following content:
1 <!DOCTYPE html>2 <html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>WebTransport</title>7 <link rel="icon" href="data:." />8 <script src="/socket.io/socket.io.js"></script>9 </head>10 <body>11 <h1>WebTransport</h1>12 <p>Connection: <span id="connection">Disconnected</span></p>13 <p>Transport: <span id="transport">Not Defined</span></p>14 </body>15 </html>
We have two paragraphs: one for the connection status and another for the data transport mechanism.
Run npm start
to start the development server. Navigate to https://localhost:3000
, accept the use of the self-signed certificate, and you’re ready to go!
Editing server.js
to Add WebSocket Support with Socket.IO
To enable WebSocket support on the server, make the following modifications:
1 // ...2 import { Server } from "socket.io";34 // ...56 const io = new Server(httpsServer);78 // Handle connections9 io.on("connection", (socket) => {10 // Log the initial transport type: pooling, websocket, or webtransport (not yet available)11 console.log(`connected with transport ${socket.conn.transport.name}`);1213 // Handle transport upgrade: pooling → websocket → webtransport14 socket.conn.on("upgrade", (transport) => {15 console.log(`transport upgraded to ${transport.name}`);16 });1718 // Handle disconnections19 socket.on("disconnect", (reason) => {20 console.log(`disconnected due to ${reason}`);21 });22 });
Editing index.html
to Add WebSocket Support on the Client
Insert the following code before the </head>
tag to include the Socket.IO library:
1 <script src="/socket.io/socket.io.js"></script>
Add the following before the </body>
tag to handle WebSocket events:
1 <script>2 const $connection = document.getElementById("connection");3 const $transport = document.getElementById("transport");45 const socket = io();67 // Handle connection8 socket.on("connect", () => {9 console.log(`connected with transport ${socket.io.engine.transport.name}`);1011 $connection.textContent = "Connected";12 $transport.textContent = socket.io.engine.transport.name;1314 // Handle transport upgrade15 socket.io.engine.on("upgrade", (transport) => {16 console.log(`transport upgraded to ${transport.name}`);1718 $transport.textContent = transport.name;19 });20 });2122 // Handle connection errors23 socket.on("connect_error", (err) => {24 console.log(`connect_error due to ${err.message}`);25 });2627 // Handle disconnections28 socket.on("disconnect", (reason) => {29 console.log(`disconnected due to ${reason}`);3031 $connection.textContent = "Disconnected";32 $transport.textContent = "Unavailable";33 });34 </script>
Restart the Server
Run the following command to restart the server:
1 npm start
Verify WebSocket Upgrade
Once the server is running, visit your application in the browser and verify that the transport is successfully upgraded to websocket
.
Next Step: Adding WebTransport Support
Extend the server.js
file to include WebTransport capabilities for enhanced transport options. Here's how to proceed:
1 // ...2 import { Http3Server } from "@fails-components/webtransport";34 // ...56 const io = new Server(httpsServer, {7 // `webtransport` must be explicitly specified8 transports: ["polling", "websocket", "webtransport"],9 });1011 // Create an HTTP/3 server12 const h3Server = new Http3Server({13 port,14 host: "0.0.0.0",15 secret: "changeit",16 cert,17 privKey: key,18 });1920 // Start the HTTP/3 server21 h3Server.startServer();2223 // Create a stream and pass it to `socket.io`24 (async () => {25 const stream = await h3Server.sessionStream("/socket.io/");26 // Familiar processing logic27 const sessionReader = stream.getReader();2829 while (true) {30 const { done, value } = await sessionReader.read();31 if (done) {32 break;33 }34 io.engine.onWebTransportSession(value);35 }36 })();
Editing index.html
Add the following to specify webtransport
as a transport option for Socket.IO:
1 <script>2 // ...34 const socket = io({5 transportOptions: {6 // Explicitly specify `webtransport`7 webtransport: {8 hostname: "127.0.0.1",9 },10 },11 });1213 // ...14 </script>
Restart the Server
Handling Certificate Issues
You might encounter an error related to an untrusted certificate. To resolve this, Chrome needs specific flags to handle HTTP/3 and QUIC protocols properly. After research, three Chrome flags are necessary:
--ignore-certificate-errors-spki-list
: Ignores SSL certificate errors for a specific certificate (requires the certificate's hash, see below).--origin-to-force-quic-on
: Forces QUIC protocol for specific origins.--user-data-dir
: Specifies a user profile data directory (required for reasons unclear).
Generating a Certificate Hash
Create a script called generate_hash.sh
to generate the certificate hash:
1 #!/bin/bash2 openssl x509 -pubkey -noout -in cert.pem |3 openssl pkey -pubin -outform der |4 openssl dgst -sha256 -binary |5 base64
Run the script:
This will produce the hash of your SSL certificate.
Launching Chrome with Necessary Flags
Create a script called open_chrome.sh to launch Chrome with the required flags:
1 #!/bin/bash2 google-chrome-stable \3 --ignore-certificate-errors-spki-list="<INSERT_HASH_HERE>" \4 --origin-to-force-quic-on="127.0.0.1:443" \5 --user-data-dir="/path/to/your/profile"
Replace <INSERT_HASH_HERE>
with the hash generated from generate_hash.sh
, and set the appropriate path for --user-data-dir
.
Important Notes:
Chrome Path Configuration
- To execute Chrome using the
chrome
command, ensure the path tochrome.exe
is included in your system's environment variable Path.
Example path:
C:\Program Files\Google\Chrome\Application\chrome.exe
Certificate Hash
The hash for --ignore-certificate-errors-spki-list
is the one generated earlier using the generate_hash.sh
script.
Run the script:
If you encounter the error chrome: command not found
, simply run the Chrome command directly in the terminal:
chrome --ignore-certificate-errors-spki-list=AbpC9VJaXAcTrUG38g2lcCqobfGecqNmdIvLV1Ukkf8= --origin-to-force-quic-on=127.0.0.1:443 --user-data-dir=quic-user-data https://localhost:443
By following these steps, you should be able to launch Chrome with the necessary configurations to work with WebTransport over QUIC.
With these adjustments, your server and client should now support WebTransport via Socket.IO.
Conclusion
By following the steps outlined in this guide, you’ve successfully set up a WebTransport server with socket.io using Node.js and enabled support for the HTTP/3 (QUIC)
protocol in Google Chrome. From generating SSL certificates to handling WebTransport connections and configuring Chrome to trust your custom setup, you’ve explored a practical implementation of this modern protocol.
WebTransport is still an emerging technology, and while it’s not yet natively supported in Node.js, tools like @fails-components/webtransport
provide a way to experiment with its capabilities. With proper configurations and the flexibility of socket.io
, you can leverage the speed and efficiency of WebTransport for real-time data transfer in modern web applications.
Keep in mind that as WebTransport evolves, more stable and production-ready solutions will likely emerge, making it even easier to integrate this protocol into your projects. For now, this setup serves as an excellent starting point for experimenting with the future of real-time communication.