MoQT Delivery Order and Priority: How the Scheduler Decides What Ships First
By moqtap team
MoQT Delivery Order and Priority
A MoQT publisher on a healthy link barely has to think about ordering — everything it produces goes out roughly as fast as it’s made. The interesting behavior starts the moment there’s contention: the encoder is producing video, audio, and a metadata track; the congestion window is smaller than the sum of what’s ready; and QUIC is asking, “which stream’s bytes do you want me to send next?”
MoQT’s answer to that question is its priority and delivery-order model. It’s what lets a stream degrade gracefully — dropping an enhancement layer while protecting the base layer, keeping audio ahead of video, catching a latecomer up newest-first — instead of just stalling. This post is about how that decision actually gets made, using the model in draft-ietf-moq-transport-18.
It builds directly on two earlier posts: objects, groups, and subgroups (the units being scheduled) and how subscriptions work (where the subscriber-side knobs are set).
The problem: too much ready, not enough pipe
Picture a sender with data queued on several QUIC streams at once:
Track: video Group 42 Subgroup 0 (base layer) ← readyTrack: video Group 42 Subgroup 1 (enhancement) ← readyTrack: audio Group 88 ← readyTrack: metadata Group 3 ← readyIf the link can’t carry all of it this instant, sending them in the wrong order has real consequences: ship the enhancement layer before the base and a loss stalls the frame you actually needed; ship metadata ahead of audio and you get an A/V glitch to save a catalog update nobody was waiting on. MoQT gives the sender a deterministic-ish way to rank these so the important bytes win.
Two priorities, one direction
MoQT carries two priority values, set by two different parties:
| Priority | Set by | Lives in | Scope |
|---|---|---|---|
| Publisher Priority | the publisher | subgroup / object headers | ranks a publisher’s own subgroups & objects against each other |
| Subscriber Priority | the subscriber | the SUBSCRIBE request | ranks a subscriber’s subscriptions against each other |
Both use the same convention: lower numeric value means higher priority — priority 0 beats priority 7. Think of it as urgency rank, not a score.
They answer different questions. Publisher Priority is the content author’s opinion — “within this track, the base layer matters more than the enhancement layer.” Subscriber Priority is the consumer’s opinion — “across everything I’ve subscribed to, audio matters more than video.” Neither alone is enough; the scheduler needs both.
There’s a third input that isn’t a priority but shapes order just as much: Group Order, Ascending or Descending. Ascending (oldest group first) is the live default. Descending (newest first) is for a subscriber that’s behind and would rather have fresh data than complete data — the same track, walked backwards. The publisher advertises a preferred order; the subscriber may request the other; the SUBSCRIBE_OK reports which one won.
The scheduling hierarchy
When multiple objects are eligible to send, the sender works down a hierarchy. Conceptually:
- Subscriber Priority — first, favor the higher-priority subscription. The subscriber’s audio subscription (priority 0) outranks its video subscription (priority 1), full stop, regardless of what’s inside either.
- Publisher Priority — within one subscription, favor the higher-priority subgroup. Base layer (priority 0) beats enhancement layer (priority 1).
- Group Order — among subgroups of equal publisher priority in different groups, follow the negotiated order: ascending means the lower Group ID ships first, descending means the higher one does.
- Subgroup ID — the final tie-break within a group: lower Subgroup ID first.
Walk the earlier example with the subscriber having set audio priority 0, video priority 1, metadata priority 5, and the publisher marking video base as 0 and enhancement as 1:
1. audio Group 88 (subscriber pri 0 — highest subscription)2. video G42 Subgroup 0 (subscriber pri 1, publisher pri 0 — base layer)3. video G42 Subgroup 1 (subscriber pri 1, publisher pri 1 — enhancement)4. metadata Group 3 (subscriber pri 5 — lowest subscription)Audio wins because its subscription is ranked highest. Between the two video subgroups — same subscription, same group — publisher priority splits them, protecting the base layer. Metadata trails everything. If bandwidth only covers the first two, the subscriber still has decodable audio and a decodable base-layer frame; only the enhancement and the metadata wait. That’s graceful degradation, and it’s the entire reason the model exists.
This is also the payoff of the subgroup design: because the base and enhancement layers are separate subgroups on separate streams, the scheduler can send one and hold the other. If they shared a stream, there’d be nothing to prioritize — QUIC would deliver them in the order they were written, head-of-line and all.
Priority is advice, not a contract
A crucial mental model correction: MoQT priority tells the sender what order to offer bytes to QUIC. It is not a delivery guarantee and it is not enforced end-to-end by the network.
- QUIC itself schedules stream data; MoQT priority maps onto QUIC’s stream-priority mechanism, but the transport is doing the actual byte-level interleaving.
- Once bytes are in flight, loss and retransmission happen underneath, indifferent to your priority values.
- A relay applies the hierarchy independently at each hop for each downstream subscriber — more on that in how relays work.
So priority reliably controls which objects get a chance to go first when the sender is choosing. It does not promise those objects arrive first, and it certainly doesn’t promise they arrive at all.
When “later” means “never”: delivery timeouts
Priority decides order; delivery timeouts decide when an object that lost the race should be abandoned rather than sent late. In real-time media, a video frame that’s 400ms late is worse than useless — it’s a frame the player has already skipped past. Sending it wastes bandwidth the next frame needs.
Draft-18 splits this into two parameters (draft-17 had a single DELIVERY_TIMEOUT):
| Parameter | Granularity | Effect when exceeded |
|---|---|---|
OBJECT_DELIVERY_TIMEOUT | per object | reset the stream once a single object exceeds its budget |
SUBGROUP_DELIVERY_TIMEOUT | per subgroup | reset the stream once the subgroup as a whole exceeds its budget |
If both are set, the smaller value applies; if both are zero, no delivery timeout is enforced and the sender will try to deliver reliably no matter how late. When a timeout fires, the stream is reset with a DELIVERY_TIMEOUT error code and those objects are simply gone — which is correct behavior for live media and catastrophic for a track that expected reliability. Set these deliberately per track; don’t inherit them by accident. (The split was one of the wire changes in draft-18.)
Timeouts and priority are two halves of one policy. Priority says “send the important thing first.” Timeouts say “and if the unimportant thing didn’t make it in time, drop it so it stops competing.” Together they’re how a MoQT stream sheds load instead of collapsing under it.
The hard edges
The model is clean for its designed-for case — the SVC base/enhancement split — and messier at the margins. A few honest caveats:
- Ties across tracks don’t stay locked. Give your audio and video subscriptions identical subscriber priority and the scheduler has no principled way to keep them in lock-step; it falls back to object receipt timing, which can drift at low latencies and desync A/V. If you need A/V aligned, don’t rely on equal priority to do it.
- Delivery order can be non-monotonic. Because priority can vary group to group, the sequence a relay emits isn’t guaranteed to be monotonically ascending or descending in group ID. Our hierarchy post walks a concrete example; the working group’s issue tracker has the live debate about whether the current rules do what implementers actually want.
- Per-subgroup granularity isn’t always enough. SVC implementers have asked for per-object “must send vs. drop if needed” semantics, because sometimes the right unit of sacrifice is a single object, not a whole subgroup. That’s an open discussion, not settled protocol.
None of this makes the model unusable — it makes it a policy surface you should understand rather than a black box you trust blindly.
How relays change the picture
Everything above describes a single sender. Add a relay and the priority values take a journey: Publisher Priority travels with the object, unmodified, all the way to the end subscriber, but each relay runs the scheduling hierarchy locally, using the priority that hop’s downstream subscriber requested. So two subscribers pulling the same track through the same relay can receive its subgroups in different orders, because each set its own subscriber priority. We unpack this in how MoQT relays work.
Debugging delivery order
Order and priority problems are some of the hardest to eyeball, because “wrong order” and “the network reordered it” look identical until you inspect the metadata:
- Enhancement layer stalling the base layer? They’re probably sharing a subgroup (and thus a stream). Split them so priority can act.
- Audio glitching under load while video stays smooth? Your subscriber priorities are inverted — video is ranked above audio. Check the priority you sent in each SUBSCRIBE.
- Objects arriving in an order that “shouldn’t happen”? Compare per-object publisher priority and subgroup IDs against the negotiated group order before assuming a bug. Non-monotonic delivery can be entirely spec-compliant.
- Bandwidth spent on frames the player already skipped? No delivery timeout is set, so the sender is faithfully delivering stale objects. Set
OBJECT_DELIVERY_TIMEOUT/SUBGROUP_DELIVERY_TIMEOUT.
moqtap surfaces the priority metadata directly. Every object and subgroup shows its publisher priority and arrival timing, so you can see the actual send order against the intended one. moqtap Desktop Pro visualizes per-subgroup priority and delivery order across a session, and monitoring tracks inter-object timing so you can catch a track that’s consistently losing the scheduling race.
Further reading
- draft-ietf-moq-transport-18 — priorities and the scheduling algorithm live in the data-plane / priorities sections
- Data streams — how objects map onto QUIC streams and datagrams
- MoQT objects, groups, and subgroups — why subgroups are the unit priority acts on
- How MoQT subscriptions work — where subscriber priority and group order are set
- How MoQT relays work — per-hop, per-subscriber scheduling
- moq-wg/moq-transport issues — the ongoing priority debates
Want to see the real send order in your own traffic? moqtap Desktop and the WebTransport Inspector decode publisher priority, subgroup IDs, and per-object timing across drafts 07 through 19 — so “why did that arrive first?” stops being a guess.