DEV Community

Mason K
Mason K

Posted on

Media over QUIC in 2026: a hands-on intro for web devs

TL;DR

Media over QUIC (MoQ) is an IETF pub/sub transport for live media running over QUIC and WebTransport. As of April 2026 WebTransport works in every major browser, and eleven vendors showed interop at NAB 2026. We will look at the mental model, open a real WebTransport connection from the browser, sketch a MoQ subscribe loop, and end with a checklist for whether you should touch it yet.

📦 Code: github.com/USER/moq-hello, replace before publishing

If you have ever built live video, you know the relay race: RTMP or SRT for ingest, HLS or DASH for scalable delivery, and WebRTC bolted on when someone needs sub-second latency. MoQ is the first credible attempt to do all three over one transport. This post is a practical orientation, not a production deploy guide, because the spec (draft-ietf-moq-transport-17, March 2026) is still a draft.

Why now

Two things changed in 2026.

  1. Browser support. WebTransport, the JS API that exposes QUIC to web apps, is now supported in Safari and iOS 26.4, joining Chromium and Firefox. That means a browser MoQ client is finally a normal web app.
  2. Interop. At NAB Show 2026, eleven vendors (Ant Media, AWS, Bitmovin, Broadpeak, CacheFly, Cloudflare, Nomad Media, Norsk, Oracle, Red5, Synamedia) demonstrated implementations that talked to each other.

🧠 The mental model

Forget request/response and forget peer sessions. MoQ is publish/subscribe:

  • A producer publishes named objects (think: media frames or chunks) into a track.
  • Subscribers subscribe to a track and receive objects as they are produced.
  • Relays sit in the middle and fan out, the same way a CDN fans out HTTP, which is what gives MoQ its scale story.

Underneath, QUIC and WebTransport hand you properties the old stack faked:

Property What you get
Independent streams No head-of-line blocking across objects
TLS 1.3 Encryption built in, not bolted on
Connection migration Wi-Fi to cellular without a teardown
0-RTT Faster resumption for returning viewers
Streams + datagrams Reliable or unreliable, your choice per use case

1. Open a WebTransport connection

You do not need a MoQ library to feel the transport. Here is a minimal WebTransport client. This is the layer MoQ builds on.

// app/moq/transport.js
// Requires a server speaking HTTP/3 + WebTransport on this URL.
export async function connect(url) {
  const transport = new WebTransport(url); // e.g. "https://localhost:4443/moq"
  await transport.ready;
  console.log("WebTransport session is up");

  transport.closed
    .then(() => console.log("session closed cleanly"))
    .catch((err) => console.error("session closed with error", err));

  return transport;
}
Enter fullscreen mode Exit fullscreen mode

Check support before you call it, because even in 2026 you will meet old browsers:

// app/moq/support.js
export function webTransportSupported() {
  return typeof window !== "undefined" && "WebTransport" in window;
}
Enter fullscreen mode Exit fullscreen mode

💡 Tip: WebTransport requires HTTP/3 and a valid TLS certificate. For local dev you typically run the server with a known cert hash and pass serverCertificateHashes to the WebTransport constructor. Self-signed shortcuts that worked for WebSocket dev do not apply here.

2. Read objects off a stream

Once the session is ready, you work with streams. A subscriber typically accepts incoming unidirectional streams (the relay pushing objects to you) and reads bytes:

// app/moq/reader.js
export async function readIncoming(transport, onChunk) {
  const reader = transport.incomingUnidirectionalStreams.getReader();
  while (true) {
    const { value: stream, done } = await reader.read();
    if (done) break;
    pump(stream, onChunk); // handle each stream concurrently
  }
}

async function pump(stream, onChunk) {
  const r = stream.getReader();
  while (true) {
    const { value, done } = await r.read();
    if (done) break;
    onChunk(value); // value is a Uint8Array of object bytes
  }
}
Enter fullscreen mode Exit fullscreen mode

That is the raw transport. MoQ defines the framing on top: how objects are named, grouped into tracks, prioritized, and how SUBSCRIBE and ANNOUNCE control messages flow. You do not want to hand-roll that.

3. Use a real MoQ implementation

The framing lives in the libraries. The most active implementation is the Rust stack moq-rs, and the reference material lives at moq.dev. A browser subscribe loop, conceptually, looks like this once a library wraps the WebTransport session:

// app/moq/subscribe.js  (pseudocode against a MoQ client lib)
import { MoqClient } from "some-moq-lib"; // swap for the lib you pick

export async function subscribeTrack(url, namespace, track, onObject) {
  const client = new MoqClient(url);
  await client.connect();

  const sub = await client.subscribe(namespace, track);
  for await (const object of sub) {
    onObject(object.payload); // decode + feed to your renderer
  }
}
Enter fullscreen mode Exit fullscreen mode

The decode-and-render step is its own project (WebCodecs is the usual partner for turning those payloads into frames), which is exactly why you should lean on existing implementations rather than building from the transport up.

⚠️ Should you adopt it yet?

Be honest with yourself with this checklist:

  • [ ] Do you genuinely need sub-second latency at large scale? If a few seconds is fine, LL-HLS already does that and you do not need MoQ.
  • [ ] Can you run or rent a MoQ relay? The relay tooling is young and varies by provider.
  • [ ] Are you comfortable tracking a draft spec that can still change between versions?
  • [ ] Do you have the appetite to pair MoQ with WebCodecs for actual playback?

If most of those are "no," the right move is to learn it, not ship it. Stand up the WebTransport client above, run an open MoQ implementation end to end, and get the pub/sub model into your hands.

Latency note, said plainly: the sub-second numbers floating around (versus roughly 2 to 6 seconds for LL-HLS/LL-DASH) come from demos, not contractual guarantees. MoQ does not repeal the latency-versus-buffering tradeoff. What it changes is that the low-latency path and the scalable path become the same path.

Common gotchas

The transport is new enough that the errors are not yet on Stack Overflow. Here are the ones that cost me time:

Symptom Cause Fix
WebTransport is not defined old browser, or non-secure context serve over HTTPS, test on a current Chromium/Firefox/Safari
Connection hangs on transport.ready server not actually on HTTP/3 confirm the server speaks HTTP/3 + WebTransport, not WebSocket
CERTIFICATE_VERIFY_FAILED in dev self-signed cert not pinned pass serverCertificateHashes to the WebTransport constructor
Streams open but no objects arrive subscribed to the wrong track/namespace log ANNOUNCE messages; verify names match the publisher
// app/moq/dev-connect.js: pinning a dev cert hash so localhost works
const transport = new WebTransport("https://localhost:4443/moq", {
  serverCertificateHashes: [{
    algorithm: "sha-256",
    value: hashBytes, // the cert's SHA-256, from your dev server output
  }],
});
await transport.ready;
Enter fullscreen mode Exit fullscreen mode

💡 Tip: keep a tiny echo publisher running while you build the client. Debugging the subscriber against a known-good stream is far easier than debugging both ends at once.

A quick reality check on where this fits versus what you already run:

Need Use today
A few seconds of latency, huge scale LL-HLS / LL-DASH
Sub-second, small rooms WebRTC
Sub-second AND scale, future-facing MoQ (still maturing)

What's next

  • Read draft-ietf-moq-transport-17 closely enough to understand objects, groups, and tracks.
  • Clone moq-rs and run a publisher, a relay, and a browser subscriber locally.
  • Wire WebCodecs to decode the objects you receive and actually render a frame.
  • Read Cloudflare's MoQ write-up for one CDN's view of where this is heading.

You do not have to migrate anything. But the devs who picked up HLS smoothly a decade ago were the ones who had already played with it. Same move here.

Top comments (0)