MoQT Draft-23: What Changed Since Draft-22
By moqtap team
Draft-ietf-moq-transport-23 was published on 2 November 2026, a month after draft-22 (1 October). Where draft-22 changed the wire in one place, draft-23 changes it in many.
At a glance
Wire changes
- Every control message Length is a varint instead of 16 bits, bounded by a new Setup Option,
MAX_CONTROL_MESSAGE_SIZE(0x09) (#2017). FETCH_OK(0x18) is gone. A FETCH is answered withREQUEST_OK(0x07), and the end of the track is marked on the fetch stream (#2009).FORWARD(0x10) becomesPAUSED(0x11), with the values inverted (#2002).- Fetch streams gain End of Group (
0x80), End of Track (0x81) and End of Gone Range (0x40C). End of Range indicators are delta encoded, and the in-group Object ID Delta is offset by 2 (#2006, #2013, #2022, #2031). REQUEST_ERRORcarries Error Details: REDIRECT and UNSUPPORTED_PROPERTY, in a new registry (#2010, #2011).- Thirteen new session close codes take over from
PROTOCOL_VIOLATION(#1773). - UNAVAILABLE (
REQUEST_ERROR0xA) and EXPIRED_AUTH_TOKEN (PUBLISH_DONE0x7) are new; PREFIX_OVERLAP and the UNKNOWN_OBJECT_STATUS reset code are gone (#1970, #2004, #2018, #2041). NAMESPACEcarries Parameters,FILL_PARAMETERSis a count of Parameters rather than a length, and the newSUBSCRIBE_TRACKS_ID(0x36) ties each PUBLISH to the SUBSCRIBE_TRACKS that caused it (#1858, #2015, #2018).
Behaviour
- Overlapping namespace subscriptions and duplicate PUBLISH_NAMESPACEs are allowed (#2018, #2021).
- A Track Name can no longer be empty (#1954), and an Object received after it was recorded as not existing makes the Track malformed (#2022).
- REQUEST_UPDATEs are answered in order, EXPIRES is allowed in every OK, and a relative Location filter in FETCH is an error (#1938, #1994, #1960).
The ALPN is moqt-23.
1. Control message framing
Draft-22 framed every control message with a 16-bit Length, which capped a message at 65,535 bytes. Draft-23 §9 makes it a varint:
draft-22 draft-23MOQT Control Message { MOQT Control Message { Message Type (vi64), Message Type (vi64), Message Length (16), Message Length (vi64), Message Body (..), Message Body (..),} }This touches every control message. An empty REQUEST_OK is 07 00 01 00 in draft-22 and 07 01 00 in draft-23. A draft-22 parser reading those draft-23 bytes takes 01 00 as a Length of 256 and loses its place, so the two drafts cannot share a control message parser.
The limit is negotiated in SETUP. Per §9.1.8, MAX_CONTROL_MESSAGE_SIZE “MUST be at least 65535. If not present, the default value is 65535. Until an endpoint receives the peer’s SETUP, it uses the default value, including for SETUP itself.” An endpoint “MUST NOT send a control message with a length larger than the peer’s MAX_CONTROL_MESSAGE_SIZE”, and one that receives such a message “MUST close the session with FIELD_LENGTH_EXCEEDED” (§9).
2. FETCH is answered with REQUEST_OK
Table 5 now lists 0x18 as RESERVED (FETCH_OK in <= 22), and §9.3 adds FETCH to the requests REQUEST_OK answers. The name FETCH_OK survives only as shorthand for a REQUEST_OK sent in response to a FETCH (§1.5).
Draft-22’s FETCH_OK carried End Of Track and an End Location. Both are gone. §3.2 now says where the response ends: “If Largest Object is within the requested range, the FETCH response ends at Largest Object and the publisher MUST include the LARGEST_OBJECT parameter (Section 9.19.17) in FETCH_OK. Otherwise, the response ends at the requested End Location.” A FETCH_OK without LARGEST_OBJECT no longer means the track is empty. It means the response runs to the end you asked for.
| FETCH succeeds, no other parameters | Bytes |
|---|---|
draft-22 FETCH_OK, End Of Track 0, End Location {5, 3} | 18 00 04 00 05 03 00 |
draft-23, Largest Object {5, 3} is within the requested range | 07 04 01 09 05 03 |
| draft-23, the response runs to the requested End Location | 07 01 00 |
On a FETCH’s request stream, FETCH_OK and every later REQUEST_UPDATE_OK now share type 0x07, and are told apart by order: the first answers the FETCH, and “REQUEST_UPDATE_OKs MUST be returned in the order the REQUEST_UPDATEs were received.” (§9.5).
3. FORWARD becomes PAUSED
Draft-22’s FORWARD (0x10) was 1 to forward. Draft-23’s PAUSED (0x11, §9.19.18) takes “0 (unpaused) or 1 (paused)”, and when omitted “the default value is 0”. A subscription without the parameter is not paused in either draft.
| Pause a subscription (REQUEST_UPDATE, Request ID 4) | Bytes |
|---|---|
| draft-22 | 02 00 04 04 01 10 00 |
| draft-23 | 02 04 04 01 11 01 |
Code that maps both to one field without inverting the value reports the opposite state. 0x10 is an unknown parameter in draft-23 and closes the session with INVALID_PARAMETER.
4. Fetch streams
End of Group and End of Track. Table 8 (§11.4.1) adds two indicators, 0x80 End of Group and 0x81 End of Track. “When Serialization Flags is 0x80 (End of Group) or 0x81 (End of Track), no other fields are present”, and “End of Track also indicates End of Group.” (§11.4.2.2). Per §3.2.2, “The publisher MUST follow an Object known to end the Track with End of Track, and MUST follow the last Object in the response with End of Group if it is known to end its Group”. Both values are above 127, so each takes two bytes on the wire: End of Track is 80 81.
The Gone state. §2.1.2 adds a fourth Object state: “The original publisher cannot deliver the Object and will not be able to, but does not know whether it exists.” A player meets it when a publisher restarts without state or keeps only recent history, and signals earlier Objects as Gone in FETCH responses. Gone is signalled with End of Gone Range, 0x40C (§11.4.2), and a relay signals it only when it received that signal from upstream (§3.2.3).
Objects that were recorded as missing. Draft-22 allowed an Object to arrive after it had been recorded as not existing: “This is not a protocol error and the Track is not malformed.” Draft-23 deletes that sentence, and §12.1 adds a malformed-track condition: “An Object is received after it has been recorded as not existing, or is signalled as not existing after it was received”. An Object recorded as Gone or Unknown can still arrive without error (§2.1.2). A publisher asked for Objects whose state it does not know sends an End of Unknown Range and keeps serving the rest (§3.2.2), instead of resetting the stream with UNKNOWN_OBJECT_STATUS, which is gone.
New ID arithmetic. Within a Group, §11.4.1.1 now computes “the prior Object’s ID plus the Object ID Delta + 2”. A directly following Object still carries no delta, so the change shows only where Object IDs skip: after {5, 3}, the Object {5, 7} in the same subgroup is 05 04 02 41 42 in draft-22 and 05 02 02 41 42 in draft-23. End of Range indicators are delta encoded too (§11.4.2.1): the first after the FETCH_HEADER is absolute, later ones are relative to the prior entry, and “If Group ID Delta is zero, the Object ID is the prior Object ID plus Object ID Delta + 1.” Draft-22 fetch data has to be re-encoded, not relabelled.
5. Errors
Session close codes
Draft-22 closed most broken sessions with PROTOCOL_VIOLATION (0x3). Draft-23 §12.2 defines thirteen specific codes and keeps PROTOCOL_VIOLATION for when “no more specific code applies”:
| Code | Name | Meaning |
|---|---|---|
0x200 | INVALID_STREAM_HEADER | Unknown stream or datagram type, or invalid Type Flags |
0x201 | MALFORMED_OBJECT | An Object cannot be decoded |
0x202 | INVALID_OBJECT_STATUS | Invalid Object Status, or Properties on a non-Normal Object |
0x203 | TRUNCATED_OBJECT | A data stream ended in the middle of an Object |
0x300 | MALFORMED_MESSAGE | A control message cannot be parsed |
0x301 | INVALID_FIELD | A field value that is not allowed |
0x302 | INVALID_TRACK_NAMESPACE | A Track Namespace does not conform to §8.7 |
0x303 | UNEXPECTED_MESSAGE | Unknown type, or wrong stream, order or count |
0x304 | ROLE_VIOLATION | Not allowed for the peer’s role |
0x305 | INVALID_PARAMETER | A parameter unknown, duplicated, not allowed or out of range |
0x306 | CLOSED_CONTROL_STREAM | The peer closed the control stream |
0x400 | DELTA_ENCODING_OVERFLOW | A delta-decoded value out of range |
0x401 | FIELD_LENGTH_EXCEEDED | A field longer than its maximum |
The ranges are categories: 0x2xx for data streams and datagrams, 0x3xx for control messages, 0x4xx for either, so an unrecognized code can still be read by its range. When more than one code applies, “an endpoint uses DELTA_ENCODING_OVERFLOW or FIELD_LENGTH_EXCEEDED in preference to the other codes.”
0x6 is renamed from KEY_VALUE_FORMATTING_ERROR to MALFORMED_KEY_VALUE, with the same value. A token that cannot be decoded now closes with MALFORMED_AUTH_TOKEN (0x16) instead of 0x6 (§8.9). If your player logs close codes, expect values above 0xFF.
REQUEST_ERROR Error Details
Draft-22’s REQUEST_ERROR ended with an optional Redirect structure. Draft-23 §9.4.1 replaces it with Error Details, Key-Value-Pairs running to the end of the message, and “Receivers MUST ignore unknown Error Detail types” (§9.4.2). Two are defined, in the new Error Detail Types registry (§16.11):
- REDIRECT (
0x01, §9.4.2.1) carries the Connect URI, Track Namespace and Track Name of the old Redirect structure. It “MUST be present when the Error Code is REDIRECT.” - UNSUPPORTED_PROPERTY (
0x02, §9.4.2.2) names a Mandatory Track Property the sender does not understand. A rejection with UNSUPPORTED_EXTENSION should carry one for each such Property.
New and removed codes
- UNAVAILABLE (
REQUEST_ERROR0xA, §12.3): “The responder cannot fulfill this request at this time, but might be able to later”, with a Retry Interval saying when to try again. A relay that cannot forward a request upstream yet uses it (§7.4). - EXPIRED_AUTH_TOKEN (
PUBLISH_DONE0x7, §12.4) ends a subscription whose authorization token expired (§9.9), so a player can tell that apart from any other reason its subscription ended. - PREFIX_OVERLAP (
REQUEST_ERROR0x30) is removed, because overlapping namespace subscriptions are now allowed (section 6). - UNKNOWN_OBJECT_STATUS (stream reset
0x6) is removed (section 4).
6. Namespaces and SUBSCRIBE_TRACKS
- Overlap is allowed (#2018). Draft-22 rejected a SUBSCRIBE_NAMESPACE or SUBSCRIBE_TRACKS whose prefix overlapped an existing one. In draft-23, “If a Track matches more than one SUBSCRIBE_TRACKS, the publisher sends a PUBLISH for each.” (§3.6). Each carries the new
SUBSCRIBE_TRACKS_IDparameter (0x36, §9.19.21), the Request ID of the SUBSCRIBE_TRACKS it answers. Cancelling a SUBSCRIBE_TRACKS leaves existing subscriptions in place (§3.6). - Duplicate PUBLISH_NAMESPACE (#2021). A publisher may send several for the same prefix, and “The namespace remains published until all of them are withdrawn.” (§4.1).
- NAMESPACE carries Parameters (#1858) after the suffix (§9.15). When they change, the NAMESPACE is sent again, so a second NAMESPACE for the same suffix is an update, not a new namespace.
- Prefix updates (#2019). “For a SUBSCRIBE_NAMESPACE, the REQUEST_OK implies a NAMESPACE_DONE for each advertised namespace that no longer matches the prefix.” (§9.5.2).
- SUBSCRIBE_TRACKS parameters (#1995) are listed explicitly in §9.17, now including the delivery timeouts, SUBSCRIBER_PRIORITY, LOCATION_FILTER and FILL_PARAMETERS, but not RENDEZVOUS_TIMEOUT or NEW_GROUP_REQUEST (§3.6.2).
- FILL_PARAMETERS (#2007, #2015) may also appear in PUBLISH and SUBSCRIBE_TRACKS, and its value is now “a Number of Parameters (vi64) followed by that many Parameters” (§9.19.15) instead of a byte length.
7. Smaller rules
- Track Names cannot be empty (#1954). “If an endpoint receives a Track Name with a length of 0, it MUST close the session with a PROTOCOL_VIOLATION.” (§2.4.1).
- EXPIRES in every OK (#1994). It “MAY appear in PUBLISH, SUBSCRIBE_OK, FETCH_OK, or any REQUEST_OK” (§9.19.16).
- No relative filter in FETCH (#1960). A relative Location filter gets REQUEST_ERROR with INVALID_RANGE (§3.2).
- REQUEST_UPDATE order (#1938, #2042). REQUEST_UPDATEs are processed and answered in order (§9.5), and one that carries FILL_PARAMETERS “cannot be coalesced with other REQUEST_UPDATEs” (§9.19.15).
- Request IDs (#1993). A receiver that detects a reused Request ID closes with INVALID_REQUEST_ID, but no longer has to detect every one (§6.4.2.1).
- Properties (#2039, #2044). A supported Property outside its scope, or repeated when it allows one instance, makes the Track malformed (§8.4, §12.1). A relay that does not support a Property “MUST allow multiple instances of the Property.”
- LARGEST_OBJECT from relays (#1978). A relay computes it only from an upstream subscription that is unpaused and at least as wide as the downstream request (§7.5).
8. Renumbering
Deleting FETCH_OK (draft-22 §9.12) moves every later message section and the parameter sections down one:
| draft-22 | draft-23 |
|---|---|
| §9.13 to §9.19 (TRACK_STATUS to PUBLISH_SKIPPED) | §9.12 to §9.18 |
| §9.20.N (Control Message Parameters) | §9.19.N |
| §9.20.21 INCLUDE_PROPERTIES | §9.19.22 |
| §7.4 to §7.8 | §7.5 to §7.9 (new §7.4) |
| §9.4.1 Redirect, §9.4.2 Message Format | §9.4.2.1 REDIRECT, §9.4.1 Message Format |
| §11.4.1.2 End of Range | §11.4.2, §11.4.2.1 and §11.4.2.2 |
| §16.11.N (Error Codes) | §16.12.N (new §16.11 Error Detail Types) |
What this means for implementers
- A new control message parser: the varint Length, within the peer’s
MAX_CONTROL_MESSAGE_SIZE(65,535 until its SETUP arrives). - A new fetch reader: the response end from LARGEST_OBJECT or your requested end, End of Track on the stream, Gone ranges, and the new ID arithmetic.
PAUSED= 1 to pause, close codes up to0x401, and Error Details instead of a Redirect structure.
moqtap support
Draft-23 support ships in:
- @moqtap/codec 0.15.0: subpath
@moqtap/codec/draft23, drafts 07–23. Decode errors carry the session close code draft-23 names. - Rust crates moqtap-codec 0.10.0, moqtap-client 0.10.0 and moqtap-proxy 0.12.0: feature
draft23, inall-drafts. The client closes sessions with draft-23’s codes. - @moqtap/inspect names draft-23’s session, REQUEST_ERROR, PUBLISH_DONE and stream reset codes, and Error Detail types. @moqtap/media reads draft-23’s Object and Track properties.
- @moqtap/test-vectors 0.23.0: a new
draft23/directory of 419 vectors, 100 of them negative. - WebTransport Inspector: from its next version, sessions that negotiate
moqt-23are detected and decoded, with each FETCH paired with the REQUEST_OK that answers it.
The rendered spec is at /spec/draft-23, and the per-draft code point pages are at /reference/message-types/draft-23/ and /reference/error-codes/draft-23/, which also lists the new Error Detail Types.