Skip to main content
Version: 1.0

WebTransport

rustdocs

WebTransport is a browser API and protocol built on HTTP/3 and QUIC. It gives each session:

  • Bidirectional streams — both sides can read and write; streams are independent, so a slow one doesn't block others
  • Unidirectional streams — server-to-client or client-to-server, one-way
  • Unreliable datagrams — low-latency messages that may be dropped or reordered, like UDP

This makes WebTransport well-suited for applications where WebSockets fall short: games, real-time audio/video signaling, live telemetry, and anything where head-of-line blocking or connection setup latency matters.

WebTransport requires an HTTP/3-capable server. See HTTP/3 for setup.

Handler​

Add WebTransport to your handler chain. It intercepts incoming WebTransport CONNECT requests and hands each session to your function:

use trillium_webtransport::{WebTransport, WebTransportConnection};

let app = WebTransport::new(|wt: WebTransportConnection| async move {
while let Some(stream) = wt.accept_next_stream().await {
// handle inbound bidi or uni stream
drop(stream);
}
});

Other handlers in the chain that precede WebTransport run normally for non-WebTransport requests, so you can serve HTTP and WebTransport from the same handler tuple.

WebTransportConnection API​

Each session gets a WebTransportConnection with these methods:

MethodDescription
accept_bidi()Wait for the next client-initiated bidirectional stream
accept_uni()Wait for the next client-initiated unidirectional stream
accept_next_stream()Wait for either kind of inbound stream
open_bidi()Open a server-initiated bidirectional stream
open_uni()Open a server-initiated unidirectional stream
recv_datagram()Receive an unreliable datagram
send_datagram(&[u8])Send an unreliable datagram

Streams implement AsyncRead and AsyncWrite (bidi) or just AsyncRead (uni inbound) or AsyncWrite (uni outbound).

ℹ️ Run datagram reception in a separate concurrent task from stream acceptance. Datagrams are typically on a latency-sensitive path and shouldn't wait behind slower stream I/O.

Full server example​

use futures_lite::{AsyncReadExt, AsyncWriteExt};
use trillium_quinn::QuicConfig;
use trillium_rustls::RustlsAcceptor;
use trillium_webtransport::{InboundStream, WebTransport, WebTransportConnection};

async fn handle(wt: WebTransportConnection) {
// Echo datagrams back to the client
let datagram_loop = async {
while let Some(data) = wt.recv_datagram().await {
let _ = wt.send_datagram(&data);
}
};

// Handle inbound streams
let stream_loop = async {
while let Some(stream) = wt.accept_next_stream().await {
match stream {
InboundStream::Bidi(mut s) => {
let mut buf = Vec::new();
if s.read_to_end(&mut buf).await.is_ok_and(|n| n > 0) {
let _ = s.write_all(&buf).await; // echo
}
}
InboundStream::Uni(mut s) => {
let mut buf = Vec::new();
let _ = s.read_to_end(&mut buf).await;
}
}
}
};

futures_lite::future::zip(datagram_loop, stream_loop).await;
}

fn main() {
trillium_smol::config()
.with_acceptor(RustlsAcceptor::from_single_cert(CERT, KEY))
.with_quic(QuicConfig::from_single_cert(CERT, KEY))
.run(WebTransport::new(handle));
}

Datagram buffering​

By default, up to 16 datagrams are buffered per session. When the buffer is full, the oldest datagram is dropped to make room for the newest. Adjust this with with_max_datagram_buffer:

// "latest-only" semantics: only the most recent datagram is kept
WebTransport::new(handler).with_max_datagram_buffer(1)