Three Years of MoQT: How the Wire Protocol Evolved from Draft-00 to Draft-17
By moqtap team
Three Years of MoQT: How the Wire Protocol Evolved from Draft-00 to Draft-17
In July 2023, the IETF’s Media over QUIC (MoQ) working group published draft-ietf-moq-transport-00 — the first community draft of what would become the MoQT protocol. That initial spec defined nine message types, a single SETUP handshake, and a straightforward object delivery model on top of QUIC.
Nearly three years and 18 published drafts later, MoQT has grown into a substantially different protocol. The core vision — low-latency media delivery over QUIC — remains, but the wire format has been reshaped repeatedly as implementers discovered edge cases, the working group refined the subscription model, and the protocol matured toward its eventual RFC.
This post traces the major threads of that evolution. We focus on what changed and when, drawing only from the published drafts themselves. With two to three more drafts likely before the RFC, this is not the final chapter — but it is a useful map of how MoQT got to where it is today.
The drafts at a glance
Before diving into specific threads, here is the full timeline:
| Draft | Published | Notable changes |
|---|---|---|
| 00 | July 2023 | Initial community draft: 9 message types, single SETUP handshake |
| 01 | October 2023 | Split SETUP into CLIENT_SETUP/SERVER_SETUP, removed message length |
| 02 | January 2024 | Introduced Subscribe ID for subscription tracking |
| 03 | March 2024 | Incremental refinements |
| 04 | May 2024 | Incremental refinements |
| 05 | July 2024 | Incremental refinements |
| 06 | September 2024 | Added SUBSCRIBE_NAMESPACE, reintroduced message length |
| 07 | October 2024 | Added FETCH, renamed SUBSCRIBE_NAMESPACE to SUBSCRIBE_ANNOUNCES |
| 08 | February 2025 | Even/odd convention for extension headers, SUBSCRIBES_BLOCKED |
| 09 | March 2025 | Minor extension encoding tweak |
| 10 | March 2025 | Functionally identical to draft-09 |
| 11 | April 2025 | Subscribe ID became Request ID, 16-bit message length, even/odd for all parameters |
| 12 | June 2025 | PUBLISH control message, track_alias moved to SubscribeOk |
| 13 | July 2025 | SUBSCRIBE_ANNOUNCES renamed back to SUBSCRIBE_NAMESPACE |
| 14 | September 2025 | ANNOUNCE renamed to PUBLISH_NAMESPACE, delta-encoded object IDs |
| 15 | October 2025 | Version negotiation moved to ALPN, consolidated RequestOk/RequestError |
| 16 | January 2026 | REQUEST_UPDATE, End-of-Range flags for fetch streams |
| 17 | March 2026 | Unified SETUP message, bidirectional request streams, new varint encoding |
The pace is worth noting: drafts 09 and 10 were published two days apart in March 2025, while the gap between draft-07 (October 2024) and draft-08 (February 2025) spanned four months — one of the longer pauses in the sequence.
Thread 1: Message framing
How control messages are structured on the wire is one of the most fundamental aspects of any binary protocol. MoQT’s framing went through six distinct phases.
Draft-00 used a clean three-field structure:
Message Type (varint) + Message Length (varint) + PayloadDrafts 01 through 05 dropped the length field entirely. Messages were delimited by parsing the known fields of each message type — the receiver had to understand every message to know where one ended and the next began. This made the protocol harder to extend and debug, since unknown message types could not be skipped.
Draft 06 reintroduced the length, embedding it in the payload field using a length-prefixed binary encoding notation.
Drafts 07 through 10 returned to the explicit three-field structure from draft-00: type (varint), length (varint), payload. This was the clearest framing yet and made it possible to skip unrecognized messages.
Drafts 11 through 16 changed the length from a variable-length integer to a fixed 16-bit integer. The draft-11 changelog notes: “Control Message length is now 16 bits instead of variable length.” This capped control messages at 65,535 bytes but simplified parsing — no varint decoding needed for the length field.
Draft-17 kept the 16-bit length but replaced the QUIC varint encoding for the message type with a new variable-length integer format (vi64). The draft also moved the control stream from a single bidirectional stream to a pair of unidirectional streams.
The trajectory here tells a story: the protocol tried removing the length field early on, discovered that was impractical, and progressively converged on a design that balances extensibility (you can skip unknown messages) with parsing simplicity (fixed-width length).
Thread 2: Session establishment
Every MoQT connection begins with a handshake. The shape of that handshake changed four times.
Draft-00 used a single SETUP message (type 0x1). The client sent its supported versions; the server responded with its selected version — all within the same message type.
Drafts 01 through 10 split this into two distinct message types: CLIENT_SETUP (0x40) and SERVER_SETUP (0x41). Each side sent its own message with version information and setup parameters. This made the direction of each message explicit on the wire.
Draft-11 (April 2025) kept the two-message pattern but renumbered to 0x20 and 0x21, reserving the old 0x40/0x41 IDs. The setup parameters also adopted the even/odd encoding convention used by the rest of the protocol.
Draft-15 (October 2025) made a significant change to version negotiation itself: versions moved out of the setup messages entirely and into ALPN (Application-Layer Protocol Negotiation), handled during the QUIC/TLS handshake. The setup messages lost their version fields and became purely about exchanging parameters.
Draft-17 (March 2026) came full circle, collapsing CLIENT_SETUP and SERVER_SETUP back into a single SETUP message — this time at type 0x2F00. The old IDs (0x40, 0x41, 0x20, 0x21) are all marked RESERVED in the message type table. This reunification was possible because, with version negotiation handled by ALPN and requests moved to bidirectional streams, the two sides no longer needed distinct message types.
The full arc — SETUP, split into two, renumbered, stripped of version negotiation, reunified — illustrates how the protocol’s session model was shaped by practical constraints that emerged over time.
Thread 3: Identity and addressing
How MoQT identifies and tracks active subscriptions went through a major conceptual shift.
Drafts 00 and 01 had no explicit subscription identifier. Subscriptions were identified by their track namespace and name.
Draft-02 (January 2024) introduced Subscribe ID — a numeric identifier assigned by the subscriber to correlate requests with responses. This was the beginning of flow-controlled subscription management: a subscriber could only have as many active subscriptions as the peer allowed via MAX_SUBSCRIBE_ID.
Subscribe ID remained the core addressing mechanism for eight drafts, from draft-02 through draft-10. It appeared in SUBSCRIBE, SUBSCRIBE_OK, SUBSCRIBE_ERROR, UNSUBSCRIBE, and related messages.
Draft-11 (April 2025) renamed it to Request ID and broadened its scope. The changelog explains: “Subscribe ID became Request ID, and was added to most control messages. Request ID is used to correlate OK/ERROR responses for ANNOUNCE, SUBSCRIBE_ANNOUNCES, and TRACK_STATUS.” What had been a subscription-specific identifier became a general-purpose request correlation mechanism. MAX_SUBSCRIBE_ID became MAX_REQUEST_ID; SUBSCRIBES_BLOCKED became REQUESTS_BLOCKED.
Draft-15 (October 2025) took the generalization further by introducing consolidated response messages: RequestOk (0x07) and RequestError (0x05) replaced the per-message-type ok/error variants. Instead of separate SubscribeOk, AnnounceOk, FetchOk, etc., a single response type could acknowledge any request.
Draft-17 (March 2026) completed the transformation by moving requests to bidirectional streams. Since each request now travels on its own stream, the response comes back on the same stream — making explicit Request IDs in responses redundant. The draft notes: “Only request messages include a Request ID; response messages do not, since they are sent on the same bidirectional stream as the request.” Request IDs still exist in request messages (for flow control and dependency tracking via the new Required Request ID Delta field), but they no longer appear in responses.
Thread 4: The publisher-subscriber model
The relationship between publishers and subscribers evolved from a simple announce/subscribe pattern into a richer bidirectional model.
Drafts 00 through 05 had a basic pattern: publishers used ANNOUNCE to advertise track namespaces, and subscribers used SUBSCRIBE to request specific tracks. There was no mechanism for a subscriber to express interest in future announcements from a namespace prefix.
Draft-06 (September 2024) introduced SUBSCRIBE_NAMESPACE — a way for subscribers to register interest in announcements matching a namespace prefix, rather than waiting for specific announcements and then subscribing.
Draft-07 (October 2024) renamed SUBSCRIBE_NAMESPACE to SUBSCRIBE_ANNOUNCES and added FETCH — a request for historical data (past objects) as opposed to the live-edge subscription model. FETCH was a significant expansion of the protocol’s capabilities, enabling catch-up and replay use cases.
Draft-12 (June 2025) introduced publisher-initiated subscriptions with three new control messages: PUBLISH, PUBLISH_OK, and PUBLISH_ERROR. Before this, only subscribers could initiate data flow. The draft-12 changelog notes: “Add PUBLISH for publisher initiated subscriptions.”
Draft-13 (July 2025) renamed SUBSCRIBE_ANNOUNCES back to SUBSCRIBE_NAMESPACE — restoring the original name from draft-06.
Draft-14 (September 2025) renamed ANNOUNCE to PUBLISH_NAMESPACE, reflecting the conceptual shift: what was originally framed as “announcing” availability became “publishing” into a namespace. The changelog states: “Rename ANNOUNCE to PUBLISH_NAMESPACE.”
Across these changes, the model moved from a publisher-announces/subscriber-requests pattern toward a more symmetric design where either side can initiate data flow and express namespace-level interest.
Thread 5: Data stream encoding
While control messages handle signaling, the bulk of MoQT traffic flows through data streams carrying media objects. The encoding of these streams saw some of the most technically intricate changes.
Drafts 00 through 07 used relatively straightforward object encoding with absolute IDs — each object in a stream carried its full Group ID and Object ID.
Draft-08 (February 2025) introduced the even/odd convention for extension headers: even type IDs are followed by a single varint value, odd type IDs are followed by a length-prefixed byte sequence. This allowed compact encoding of common extensions while preserving flexibility for complex ones.
Draft-11 (April 2025) generalized even/odd encoding to all parameters, not just extension headers. The subgroup data stream types were also restructured from two variants to six (types 0x08 through 0x0D), using bit flags to indicate which optional fields (extensions, subgroup ID) are present.
Draft-12 (June 2025) further expanded the subgroup type range to twelve variants (0x10–0x15, 0x18–0x1D), adding more combinations of optional fields.
Draft-14 (September 2025) introduced delta-encoded Object IDs within subgroup streams. Instead of each object carrying its absolute Object ID, it carries an Object ID Delta — the difference from the previous object in the stream. The spec explains: “The Object ID Delta + 1 is added to the previous Object ID in the Subgroup stream if there was one.” This reduced per-object overhead for the common case of sequential objects.
Draft-16 (January 2026) extended delta encoding to key-value pairs used for parameters and headers, and introduced End-of-Range flags (0x8C, 0x10C) for fetch streams — special flag values (>= 0x80) that signal boundary conditions without carrying an object payload.
Draft-17 (March 2026) further extended delta encoding to Request IDs and filter end groups, and introduced a new variable-length integer encoding (vi64) replacing the QUIC varint format used since draft-00.
The data stream encoding evolution reflects a consistent theme: reducing per-object wire overhead while increasing the expressiveness of stream metadata. Each change made the encoding more compact but also more stateful — a tradeoff that implementers navigated draft by draft.
The near-identical drafts
Not every draft brought meaningful change. Drafts 09 and 10, published just two days apart in March 2025, are functionally identical on the wire. The differences amount to a handful of non-substantive edits — the wire protocol, message IDs, and encoding rules are the same. For implementers, supporting draft-09 and draft-10 is the same work.
Drafts 12 and 13 (June and July 2025) are also closely related, with draft-13’s changes largely consisting of the SUBSCRIBE_ANNOUNCES-to-SUBSCRIBE_NAMESPACE rename and a restructuring of track status messages into a request/ok/error pattern.
Looking ahead
As the protocol approaches its RFC milestone, the rate of change has not slowed. Draft-17 introduced a new integer encoding format, reunified the SETUP message, and moved to bidirectional request streams — each of these is a significant change for implementers.
With two to three more drafts likely before the RFC, the wire format will continue to evolve. But the overall trajectory is clear: MoQT has moved from a simple pub/sub protocol with absolute addressing toward a sophisticated, delta-encoded, bidirectional request model with ALPN-based versioning and symmetric publisher/subscriber roles.
For those implementing or debugging MoQT across draft versions, moqtap supports drafts 07 through 17 and tracks each wire format change as new drafts are published. Understanding which draft your endpoints speak — and what changed between them — is the first step in diagnosing interoperability issues.