Recovering after a publisher stops or restarts
Replacing a publisher connection restores an upstream source for new subscriptions. It does not move existing subscribers to that source (moq-relay 0.15.8 and 0.17.0 never do), and a new subscription need not succeed immediately. These are relay lifecycle limits tracked in #34. The connection primitives do not automatically reconnect or resubscribe. The opt-in supervised connections implement the policy below for live applications: a fresh generation namespace per publisher connection, wants that retry and resubscribe, a stale timer, and generation-following through discovery. The rest of this page is what they do, and what an application without them must do itself.
What a subscriber can observe
Section titled “What a subscriber can observe”| Signal | What it establishes |
|---|---|
onSubscribed / on_subscribed |
The relay accepted a subscription. It does not establish that the publisher is still producing live data. |
onObject / on_object |
An object arrived. It may be cached; arrival alone does not establish freshness. |
onPublisherDone / on_publisher_done |
The relay sent PUBLISH_DONE. Trailing object streams may still arrive until cancellation or connection closure. |
onSubgroupReset / on_subgroup_reset |
One subgroup was reset. It does not establish that the entire publisher has stopped. |
Connection closed |
This subscriber’s transport/session ended. A publisher’s separate connection can die while the subscriber connection remains open. |
requestTimeoutMs / request_timeout bounds only the initial subscription response. Once
SUBSCRIBE_OK arrives, this timer is disarmed. It is not a time-to-first-object deadline or an
inactivity detector. Transport keepalive likewise does not prove the publisher is alive.
Measured on mux.global’s moq-relay 0.17.0 on 2026-10-05 with the Python SDK (publisher restarts):
- A publisher that closes cleanly ends existing subscriptions within 0.1 s with status 2
track ended. A publisher whose connection closes without MoQ cleanup ends them with status 0internal error. - After a publisher crash, existing subscriptions sit silent for about 11 s, the relay’s QUIC idle timeout, before done(0) (30-35 s on 0.15.8). The subscriber connection stays open throughout. New subscriptions in that interval are accepted but receive only the dead publisher’s cached last object, and a replacement that announces during it does not fix that reliably: in 3 of 6 runs new subscriptions went on getting the dead publisher’s object until the timeout, because the relay picks between the two announcements effectively at random (duplicate announce).
- Existing subscriptions are never moved to a replacement publisher on the same namespace, however the old one ended.
- After a clean end, new subscriptions fail with request error 16
unroutableuntil the replacement announces, and are served by it at once from then on. - While a namespace stays announced after one of its tracks ended cleanly, re-subscribing to that
track replays its cached last group followed by another done, one round trip later each time.
Once the finished track has gone unread for about 30 s the relay forgets it, and a subscribe gets
request error 16
not found.
See issue #34 and issue #21 for the measurements. They do not define a recovery deadline.
An orderly publisher close helps the relay learn about shutdown sooner, but neither graceful closure nor successful replacement writes promise transparent handoff. Write completion only means the transport accepted the bytes.
Application recovery policy
Section titled “Application recovery policy”A live application that needs faster recovery must define freshness and own the recovery loop. A useful policy is:
- Track time to first usable object and time since the last fresh object with a monotonic local clock. Choose thresholds for the content’s expected cadence and tolerated interruption. A quiet catalog, sparse event track, paused source, or long GOP is not evidence of failure by itself. Where silence is valid, use an application heartbeat or an external publisher-health signal.
- When progress stops, report a stalled state and schedule one recovery attempt. Do not wait only
for
onPublisherDone/on_publisher_done: it can be delayed while the relay detects a dead upstream connection. Keep an overall recovery deadline and a cancellation path. Do not create a new loop on every callback. - Retire the old subscription before accumulating replacement attempts. For live playback,
cancelling it deliberately discards any trailing data so the application can prefer a fresh
random-access point. A consumer that needs trailing data must choose its own bounded drain
policy before cancelling.
onPublisherDone/on_publisher_donealone does not prove the data is drained. - Request the desired live track again. The subscriber connection can stay open. If it has closed,
open a replacement connection first. A new connection is not a guaranteed bypass for relay state.
During a known publisher transition, classify the fresh-namespace rejection (16
unroutable, or404on older relays) and initial-response timeouts as potentially transient and retry within the recovery budget. After a crash, a subscription accepted before the relay’s idle timeout may serve only the old publisher’s cached object, even after the replacement announces. Check freshness rather than trusting acceptance, and retry a stale subscription with backoff until the new generation serves it. Do not re-subscribe to a track the publisher ended while it is still announced: that only replays the finished track. A status-2 done alone does not prove the track ended, because moq-relay also sends one after the subscription’s ownupdate(). In that case re-subscribing is the recovery. Authentication, invalid input, and protocol failures need their own handling rather than an unlimited retry loop. - Apply backoff and jitter across failed attempts and accepted-but-stale attempts. Cached
objects followed by
onPublisherDone/on_publisher_donemust not reset the budget or create an immediate resubscribe loop. Reset only after meaningful fresh progress. See the cached-track resubscription issue. - On a media publisher generation change, reacquire the catalog/configuration and resume at a usable keyframe. Group/object IDs can restart. They are not an application continuity identifier. If continuity matters, include a publisher generation and sequence in the application payload and define how to reject replay, handle gaps, and identify a new generation.
The SDK cannot choose these thresholds or interpret these payloads for a raw byte track. This
policy belongs in the application or player. Changing a request timeout will not implement it.
A supervised subscriber’s staleAfterMs is that freshness threshold. It is set per track by the
application, because only the application knows which tracks may be silent.
When predictable handoff matters
Section titled “When predictable handoff matters”Assign each publisher generation a fresh namespace under the authorized root, and let viewers find
it with discovery: namespaceEvents / namespace_events on the root
reports each generation announced when it announces, and unannounced when it closes (within
about 400 ms) or crashes (about 12 s later, at the relay’s QUIC idle timeout). Viewers can then
explicitly switch to the new generation and cancel the old subscriptions. This avoids relying on
reuse of the ended namespace’s cached state. A subscribe on the discovering connection once a new
generation is announced routes at once: on moq-relay 0.17.0 it was never rejected unroutable
(0 of 160 trials on 2026-10-06, on the publisher’s relay and on another;
announce propagation), so a viewer that switches on
announced needs no timed retry for the announce race. A crashed generation stays announced
until the timeout, so freshness checks and a bounded switch deadline remain necessary. A viewer that
subscribes without waiting for discovery, at startup or to a fixed namespace, keeps the startup retry.
Discovery reports namespaces in arrival order, not by generation. A
supervised publisher names its generations so they sort by start
time, and a supervised subscriber’s want with generations: true follows the newest one announced,
so which generation is newest needs no application channel either.
For a fixed namespace, keep the recovery policy above and budget for relay convergence. SDK-side retry can mitigate a stall, but cannot rebind an existing subscription at the relay or guarantee a handoff latency. Prompt upstream-death notification, replacement-source routing, and stale state after retract depend on the relay.