MoQT Objects, Groups, and Subgroups: The Hierarchy and Why It Exists
By moqtap team
MoQT Objects, Groups, and Subgroups: The Hierarchy and Why It Exists
Open any MoQT spec and one of the first things you’ll see is the data model: a track contains groups, a group contains objects, and objects can be organized into subgroups. Four nouns, one diagram, easy.
The shape is deceptively simple. Each level was added — and reworked draft by draft — to solve a real problem. Groups give subscribers a join point. Subgroups didn’t even exist until draft-06 (per IETF 121 slides: “Draft-05 predates subgroups”). Priorities have been rewritten more than once. The whole thing is the residue of two and a half years of arguing about how to ship video, audio, logs, and sensor data over one protocol without any of them ruining the others.
The hierarchy at a glance
Track├── Group 0│ ├── Subgroup 0│ │ ├── Object 0│ │ ├── Object 1│ │ └── ...│ └── Subgroup 1│ ├── Object 0│ └── ...├── Group 1│ └── ...└── ...A track is the unit of subscription — a named, ordered sequence of objects identified by a namespace plus a track name. Everything else lives inside a track.
A group is a sequence of objects that should be treated as a unit. From draft-ietf-moq-transport-17: “Groups SHOULD be independently useful, so Objects within a Group SHOULD NOT depend on Objects in other Groups. A Group provides a join point for subscriptions.” That last sentence is the whole reason groups exist.
A subgroup is a sequence of objects within a group that share a dependency relationship and a priority, and are sent on a single QUIC stream whenever possible. Subgroups don’t have to exist — a group can hold one subgroup or many. They’re a tool you reach for when you need intra-group priority or parallel delivery.
An object is the leaf. A single application-level payload: a frame, a message, a sample, a log record. Objects carry an ID, a status, a priority, and optional extension data, plus the payload itself.
Objects: the leaf
An object is the smallest addressable unit in MoQT. The protocol doesn’t care what’s inside — the spec is explicit that “despite its name, MOQT is media agnostic and can be used for a wide range of use cases.”
What MoQT does care about is metadata. Each object on a stream carries:
- Object ID within its group (delta-encoded in draft-14 and later)
- Publisher priority (or it inherits the subgroup’s default)
- Object status — normal, end-of-group, end-of-track
- Extension data — opaque application bytes
- Payload length + payload
The receiver uses these to schedule, route, and deliver the payload without ever decoding it. A relay doesn’t need to know that an object is a video frame or a chat message to forward it correctly; the metadata is enough.
Groups: the join point
The group exists for one reason: a subscriber who shows up partway through a track needs a place to start.
If you’re subscribing to a live video stream, you can’t start decoding at an arbitrary P-frame — you need an IDR. So the publisher arranges its objects so that group boundaries coincide with random-access points. From the LOC draft: “Each independently coded sequence of pictures in a resolution is sent as a group as the first picture in the sequence can be used as a random access point.”
That convention — group = GOP, Object 0 = IDR — is now the de facto pattern for video over MoQT.
For audio, the LOC draft’s Opus example is one object per group: “each encoded audio chunk (say 10ms) represents a MOQT Object. In this setup, there is one MOQT Object per MOQT Group, where the GroupID in the object header is incremented by one for each encoded audio chunk.” Every 10ms is a join point, which matches how cheap audio decoding is to bootstrap.
In practice a lot of A/V publishers do something different: they bundle audio into groups that match the video GOP. An audio group then holds however many frames cover one video GOP — usually a fluctuating count, ±1 or 2 between groups. The point is sync. Subscribe to the latest group on both tracks and they land on the same boundary, with PTS lining up at object 0. With per-chunk audio groups the player has to reconstruct that alignment itself.
For chat or sports score tickers, each message is its own group with one object. Each message is independently consumable; there’s nothing to “join into.”
For logs, draft-jennings-moq-log goes further and encodes time directly: “GroupID = microsecond timestamp truncated to 62 bits. ObjectID = 0 unless multiple messages in the same microsecond.” Every log line is its own group, with the timestamp baked into the ID space.
The group is also the unit a SUBSCRIBE filter operates on. Filters like LatestObject, NextGroupStart, AbsoluteStart, and AbsoluteRange all talk in terms of group boundaries — because that’s where it’s safe to start.
Subgroups: the priority lever
Subgroups are the most interesting level of the hierarchy because they’re the one that wasn’t there at the start.
Pre-draft-06, a group mapped to a single QUIC stream. Every object in the group went down that one stream, in order. This worked fine for plain video, but it broke down for two cases the working group cared about: SVC and independent-priority objects within a group.
The SVC motivation
Scalable Video Coding splits a video stream into a base layer (lower frame rate, decodable on its own) and one or more enhancement layers (higher frame rate, dependent on the base). If you put both into a single QUIC stream in alternating order, you’ve created a head-of-line problem: a lost packet carrying an enhancement-layer frame will stall the base layer that follows it.
Subgroups fix this. Put the base layer in subgroup 0 and the enhancement layer in subgroup 1, each on its own stream, and a loss in the enhancement subgroup never blocks the base. From draft-17: “The temporal layers are sent as separate subgroups to allow the priority mechanism to favor lower temporal layers when there is not enough bandwidth to send all temporal layers.”
The LOC draft makes this concrete with H.264 at 30/60 fps:
| Subgroup | Layer | ObjectIDs | Stream |
|---|---|---|---|
| 0 | Layer 0 / 30 fps | even (0, 2, 4, …) | one QUIC stream |
| 1 | Layer 1 / 60 fps | odd (1, 3, 5, …) | one QUIC stream |
The IDR is always ObjectID 0 in subgroup 0. The base layer is always decodable; the enhancement layer is a bonus that can be dropped when bandwidth tightens.
Simulcast is not subgroups
A common mistake is to use subgroups for simulcast — different resolutions of the same content. The working group landed on a different answer: simulcast variants are separate tracks, not separate subgroups.
The common catalog format tags tracks with group and alt-group integers so a player knows which tracks render together and which are alternates. A 360p track and a 720p track are alt-groups; the subscriber picks one at a group boundary and can switch later.
draft-jennings-moq-usages spells out the distinction: simulcast tracks are “time-aligned alternate encodings of the same source content, allowing consumers to switch between tracks at group boundaries.” The decision lives at the subscription layer, not inside a group.
When to split into a new subgroup
Draft-17’s rule of thumb for whether two objects belong together: “If an Object is dependent on all previous Objects in a Subgroup, it likely fits best in that Subgroup; if it is not dependent on any of the Objects in a Subgroup, it likely belongs in a different Subgroup.”
There’s a wire cost on the other side of that. More subgroups means more streams, and more streams means more overhead. The spec says as much: “the Original Publisher makes a reasonable tradeoff between having an optimal mapping of Object relationships in a Group and minimizing the number of streams used.” Issue #481 goes further and defines a STREAM_LIMIT_EXCEEDED error for publishers that have spawned too many subgroup streams — the lowest-priority subgroup gets dropped.
Per-subgroup priorities
Subgroups carry priority. All objects in a subgroup share that priority. Higher-priority subgroups are sent before lower-priority ones. If two subgroups in a group have the same priority, the lower subgroup ID wins.
That sounds simple. In practice the priority section of the spec has been rewritten more than once, and there are open issues right now about whether the current rules do what implementers actually want.
PR #518 defined the current algorithm. Issue #585 is a counter-proposal that points out the algorithm can produce non-monotonic delivery: “Group 0/Subgroup 0/Priority 1; Group 1/Subgroup 0/Priority 3; Group 2/Subgroup 0/Priority 2 — the relay will send 0, 2, 1, which is neither ascending nor descending.”
Issue #475 raises a related problem: there is no way to keep two subscriptions with identical priority in lock-step. If you subscribe to audio and video as separate tracks at the same priority, the relay falls back to object receipt time — which can drift at low latencies and desync A/V.
Issue #792, filed by an SVC implementer, makes the strongest case for finer control: they want per-object “must send” vs. “drop if needed” semantics, because per-subgroup priority isn’t always granular enough.
None of this is settled. Per-subgroup priority works well for the SVC base/enhancement split — that’s what it was designed for. A/V sync and fine-grained drop policies are still being argued out.
Streams, datagrams, and the head-of-line tradeoff
A subgroup maps to a single QUIC stream “whenever possible.” That phrase hides a real choice the publisher has to make.
QUIC streams give you reliability and ordering inside a stream, with no head-of-line blocking between streams. Lose a packet on the enhancement-layer stream and the base layer keeps flowing. Lose a packet on the base layer stream and only that subgroup stalls.
The alternative is datagrams. MoQT lets a publisher deliver objects as unreliable QUIC datagrams instead of stream-bound objects. This is the right call for ephemeral payloads where retransmission is worse than loss — audio frames, sensor samples, gaming inputs. The cost is no reliability and no ordering across datagrams, so anything larger than the path MTU is forced back onto streams.
Issue #359 debates whether the PREFER_DATAGRAM setup parameter is a hint or a hard requirement. Issue #715 asks what reliability semantics a subscription with no timeouts actually requires when delivery is via datagrams.
For datagram delivery, the subgroup concept collapses to the per-object scheduling unit — there’s no stream to share. Issue #623 pins down how subgroup IDs are encoded in this case for FETCH responses.
It isn’t only for media
MoQT is deliberately content-agnostic, and a handful of non-media uses push on the data model in interesting ways:
-
Syslog over MoQ (draft-jennings-moq-log) — devices publish per-priority log tracks. The track name is a single byte = syslog priority. Microsecond timestamp goes in the GroupID. OpenTelemetry TraceID, SpanID, and InstrumentationScope are first-class extension fields. The motivation: “the network bandwidth for logging is shared with the real time media and impacts the media quality. There is a desire to transport the logging data at an appropriate priority level over the same transport as the media.”
-
Cluster gossip routing — MoQ relays advertise themselves on
cluster.<node>tracks and subscribe to peer tracks to bootstrap their own overlay. The protocol carries its own routing plane. -
Sports score tickers, game state, IoT telemetry — all the same pattern: low-bandwidth, one-object groups, prioritized below media, sharing the same QUIC connection.
-
Event-camera streams for ML — academic work has mapped event-based video onto MOQT subgroups for analytics pipelines. The protocol doesn’t care that the payload isn’t a frame in any traditional sense.
Catalog tracks fall in the same bucket. The objects are content metadata, not media — each catalog update is typically one object. Issue #424 is a long argument about what happens when the catalog track terminates while init-segment objects are still in flight on another stream.
Debugging through the hierarchy
When a session stalls, the levels are your map:
- No groups arriving? Subscription was accepted but the publisher isn’t producing, or the relay isn’t routing.
- Groups arriving but nothing decodes? Object 0 isn’t an IDR. Encoder-mapping problem.
- Some subgroups arriving, others stalled? A subgroup stream is blocked by flow control, or the publisher hit the stream limit and dropped the lowest-priority one.
- Out-of-order delivery? Check subgroup priorities against what the application expected, and look at issue #585 before assuming the bug is yours.
- Datagram delivery with missing objects? That’s by design — the question is whether the application can take the loss.
moqtap shows the hierarchy directly. The desktop app and browser extension both render tracks as a tree of groups and subgroups, with per-object priority, status, and timing. The .moqtrace format records every object with enough context to replay it offline.
Further reading
- draft-ietf-moq-transport-17 — the current MoQT spec, with the data model in Section 2 and stream/priority semantics scattered through the data plane sections
- draft-ietf-moq-loc-01 — the canonical Low Overhead Container mapping, with concrete H.264 + temporal layer and Opus audio examples
- draft-jennings-moq-usages-00 — application-side mappings for audio, video, and simulcast
- draft-jennings-moq-log — syslog over MoQ, the most fully-developed non-media use case
- Common Catalog Format — how tracks are tagged for simulcast and joint rendering
- moq-wg/moq-transport issues — where the current debates live
If you’re building on MoQT and want to see this hierarchy in your own traffic, install the WebTransport Inspector browser extension or moqtap Desktop. Both decode every group, subgroup, and object end-to-end across drafts 07 through 18.