/live?tab=controlfor current broadcast state and operator actions/live?tab=statusfor incidents, drift checks, destination health, and audit visibility/live?tab=moderationfor moderation presence and check-in history/api/healthfor basic service health/api/system/readinessfor broadcast readiness and drift-relevant status
- worker heartbeat freshness
- playout heartbeat freshness
- destination readiness
- current asset selection reason
- transition state and next-asset probe status
- crash-loop protection state
- open critical incidents
- active SSE connections reported as
sseConnectionsin/api/system/readiness - container restart deltas in the soak monitor log
- restart encoder
- refresh overlay/slate payloads without restarting the encoder
- rebuild the visible current / next / queued runtime state on the next playout cycle
- recover staged outputs immediately instead of waiting for the next natural transition
- switch to fallback
- pin asset on air
- skip current asset
- resume schedule control
- acknowledge and resolve incidents
- open
/live?tab=status - inspect
selectionReasonCode - inspect
fallbackTier - inspect destination readiness
- inspect last FFmpeg stderr sample
- inspect
restartCount,lastExitCode, andcrashCountWindowin/api/system/readinessor the soak monitor log - distinguish planned reconnects from recovery: planned reconnects report
selectionReasonCode=scheduled_reconnect, while FFmpeg failures usually incrementrestartCountwith a signal or exit code such asSIGBUS,128, or8 - in HLS program-feed mode, treat
playoutTransient=trueas a local playout recovery window, not a Twitch reconnect, as long asuplinkStatus=running,programFeed=fresh,destination=ok, anduplinkUnplannedRestartshas not increased - when relay/HLS is enabled, a fresh
programFeed.updatedAtnow counts as active playout liveness forrunning,recovering, andswitching; do not treat a quiet FFmpeg stderr stream by itself as an outage whileprogramFeed=freshanduplinkStatus=running - if the playout container accumulates zombie FFmpeg or yt-dlp processes, recreate it: the image runs Node under
tini, which reaps them, so an accumulation means the container is not running the shipped entrypoint - if the soak monitor reports
container-restart-check-failed, inspectdocker compose ps,docker inspect --format '{{.RestartCount}}', and recent logs forweb,worker, andplayoutbefore restarting the soak
vod.cache.job.startcarriestimeoutMs(effective),configuredTimeoutMsanddurationSeconds: a background download gets at least the replay's running time, capped at one day, so a five-hour VOD is not killed at the two-hour floor.vod.cache.keptwithreason: scheduled-within-retentionmeans a replay that just finished stays on disk because a block withinretentionHoursdraws from its pool;vod.cache.releasedis the old path and still runs for everything else.playout.twitch-cache.failedwith "Command timed out" now means the download could not keep up with real time for the whole running length of the content — a network problem, not a short fuse.
- remotely streamed VODs (CloudFront-backed Twitch assets too large to cache) can reach their end without ffmpeg receiving EOF; when the asset's duration is known, the playout ends it deliberately once elapsed playback passes duration plus a margin, on the same planned-transition path a natural boundary takes
- a
playout.duration_bound.endruntime event at an asset end is that planned transition, not a fault; no incident accompanies it uplink.encoder_stall.restartorplayout.feed_audio.restartfiring at almost every asset end means the bound is not firing for those assets — check that theirdurationSecondsmetadata is present; assets with an unknown duration fall back to the watchdogs by design- tuning:
PLAYOUT_DURATION_BOUND_MARGIN_SECONDS(default 15) — seconds past the known duration before the deliberate end; keep it generous, because cutting duplicated last-frame is invisible while cutting real content is not. Since M56 part 2 this margin — like every watchdog threshold — is also settable in Admin → Settings → Operations ("Watchdog thresholds"); a value saved there wins over the env variable, and the GUI enforces the safe range (5–120 s here)
- inspect the latest playout incidents
- verify stream destination and selected asset
- request a manual restart only after the cause is understood
- inspect the destination panel in
/broadcastfor cooldown timers, staged outputs, and the latest failure sample - let the next natural transition bring staged outputs back when continuity is more important than immediate fanout recovery
- use
Recover outputs nowonly when an immediate encoder restart is acceptable
- confirm broadcaster connection
- check managed credentials or
.envfallback - review Twitch incidents in
/live?tab=status
- verify local media exists or remote sources ingest correctly
- confirm source incidents
- for Twitch VOD assets, inspect
playout.twitch-cache.failedincidents and confirmMEDIA_LIBRARY_ROOT/.stream247-cache/twitchis writable with enough free space - if playout stays on the reconnect slate while a Twitch VOD is still downloading, inspect the playout container for active
yt-dlpwork and prune leftover.part-*files for the same VOD; the production timeout should stay low enough that playout falls through to local fallback instead of waiting for a multi-minute cache prep - keep at least one curated local fallback asset under
data/mediawithfallbackorstandbyin the file name so the local-library source promotes it to a global fallback automatically - confirm the Twitch cache guardrails are set — since M56 part 2 the whole family (and the uplink
watchdog pair below) is managed-first: Admin → Settings → Operations ("Replay cache" and
"Watchdog thresholds") wins over these env values, which remain the fallback for an untouched
install. Only
TWITCH_VOD_CACHE_ROOTstays env-only, because a mount point is infrastructure:TWITCH_VOD_CACHE_DOWNLOAD_TIMEOUT_SECONDSTWITCH_VOD_CACHE_RETENTION_HOURSTWITCH_VOD_CACHE_PARTIAL_MAX_AGE_HOURSTWITCH_VOD_CACHE_MAX_ASSET_BYTES— per-VOD ceiling. A VOD above it is never downloaded and is played straight from Twitch (cacheStatus: "too-large", eventvod.cache.too_large); this is a settled decision, not a failure, so nothing retries it. KeepTWITCH_VOD_CACHE_MAX_BYTESabove it, or the prune evicts the very file being downloaded.TWITCH_VOD_CACHE_MAX_BYTESTWITCH_VOD_CACHE_MIN_FREE_BYTESUPLINK_STALL_TIMEOUT_MS/UPLINK_STALL_GRACE_MS— how long the uplink may run without advancingout_timebefore it is restarted, and the quiet period after start. A stalled ffmpeg stays alive and keeps its destinations inready, so process liveness alone does not catch it; watch for theuplink.encoder_stall.restartanduplink.encoder.no_progressevents.TWITCH_VOD_CACHE_LIMIT_RATE— caps download bandwidth (yt-dlp notation, e.g.8M). Unset means unlimited, which lets a background download saturate the same line the uplink pushes through.TWITCH_VOD_CACHE_FAILURE_COOLDOWN_SECONDS
- keep remote Twitch fallback disabled unless you intentionally accept direct remote VOD playback risk
- confirm fallback assets exist
The worker watches free space on the media volume as a whole, above the per-cache guardrails. Below the trigger watermark it evicts in stages, at most one stage per worker cycle: unused Twitch VOD cache entries first, then orphaned program-feed segments, then the oldest thumbnails. Eviction stops as soon as free space is back above the recovery watermark, and media the schedule, queue or fallback tier still references is never touched.
- a
disk.watermark.evictedwarning incident names what was freed and why; no action is needed — the system is protecting itself - a
disk.watermark.exhaustedcritical incident means every stage ran and free space is still below the recovery watermark: nothing evictable is left, so free space manually (grow the volume, remove local media, or shrink the schedule's VOD footprint) before playout, feed segments or downloads start failing writes - runtime events:
disk.watermark.stage,disk.watermark.recovered,disk.watermark.exhausted, anddisk.watermark.check_failedwhen the volume could not be measured - tuning:
STREAM247_DISK_WATERMARK_TRIGGER_PERCENT(default 10, percent free that starts an episode),STREAM247_DISK_WATERMARK_RECOVER_PERCENT(default 15, where it stops; must be above the trigger or both fall back to defaults),STREAM247_DISK_WATERMARK_ENABLED=0to disable
- confirm
STREAM247_RELAY_ENABLED=1and therelay,playout, anduplinkcontainers are running - confirm
STREAM247_UPLINK_INPUT_MODE=hlsunless you intentionally rolled back to the older MediaMTX relay input - if an upgraded worker logs
column "uplink_status" of relation "playout_runtime" does not exist, deploy a build that includes the persistent program-feed upgrade migration before restarting the soak - inspect
program-feed.input,uplink.output.missing,uplink.process.exit, anduplink.ffmpeg.stderrincidents - check
/api/system/readinessforuplink.unplannedRestartCountandprogramFeed.status - if HLS warnings mention corrupt packets, discontinuities, or non-monotonic DTS but
uplink.unplannedRestartCountstays unchanged and the feed remains fresh, investigate the local asset/input that caused the playout exit instead of reconnecting Twitch manually - single-output and multi-output RTMP uplinks both run through tee/fifo buffering now; a short Twitch-side write failure should recover inside the same FFmpeg process when FFmpeg can re-open the output, and a real
uplink.process.exitstill means the Twitch-facing publisher actually restarted - verify at least one enabled primary or backup destination has a valid RTMP URL and stream key
- use
STREAM247_RELAY_ENABLED=0only as a rollback because it returns external publishing to the playout process - since the relay checks credentials (M57 stage 2), the rtmp rollback paths (
STREAM247_RELAY_ENABLED=1,STREAM247_UPLINK_INPUT_MODE=rtmp) additionally require the internal relay key embedded inSTREAM247_RELAY_OUTPUT_URL/STREAM247_RELAY_INPUT_URL; get both lines ready to paste from Settings → Operations → Relay access (owner/admin, one click, audited asrelay.internal_key.revealed), copy them into the deployment environment, then restart — see the push ingest section indocs/deployment.md - the key never appears in a listing, a log, a scene payload or an error message, so a lost copy is re-fetched from that same group rather than recovered from anywhere else; if the group answers that the lines are unavailable, the workspace database is unreachable and that is the incident to fix first
At every programme boundary the uplink's ffmpeg derives a new timestamp offset per stream. Measured
by hand across nine boundaries on 2026-09-05, the difference between the video offset and the audio
offset at the same seam was the one number that separated a discontinuity storm from a quiet
boundary: storms 11.84–13.45 s, quiet 1.07–6.69 s, with ffmpeg's dts_delta_threshold default of
10 s in the gap. Since 1.5.47 the uplink input carries -dts_delta_threshold 60; since M61 the
number is logged instead of read off docker logs by hand:
uplink.seam.skew—skewSeconds, both offsets in microseconds, and the discontinuity line count of the current 60 s window. One line per seam.playout.feed.av_lead— at every duration-bound cut, the last audio and video packet times in the newest segment the outgoing encoder wrote, andaudioLeadSeconds. This is the writer's view of the same seam; if it is near zero while the uplink reports a large skew, the seam is the reader's doing.
To judge whether the 60 s threshold is doing its job:
docker logs stream247-uplink-1 2>&1 | grep -E "uplink.seam.skew|discontinuity-storm" | tail -20
docker logs stream247-playout-1 2>&1 | grep "playout.feed.av_lead" | tail -20A seam with skewSeconds above 10 and a discontinuityCount that stays in single digits is the
evidence the threshold works. A seam below 10 s proves nothing either way.
Existing DUT soak notes after the persistent program-feed rollout showed the web, worker, playout, and uplink containers staying healthy with Docker restart counts at zero during the observed long run. The remaining failures were playout-runtime transients, not container restarts or Twitch uplink reconnects.
For future long runs, treat the baseline as:
web,worker, andplayoutDocker restart counts should remain unchanged; the soak monitor fails if any of them increases by more than one during the soak window.uplink.unplannedRestartCountshould remain unchanged; any increase means the Twitch-facing RTMP session probably reconnected outside the planned 48-hour reconnect.sseConnectionsmay rise while operators keep Live, Channel, or Studio pages open, but it should return to zero after those clients disconnect.- playout container memory should be checked with
docker statsduring multi-day soaks; the scene renderer runs in-process (satori → resvg, no child processes), so sustained RSS growth is actionable, while stable RSS with no restart-count increase is the expected baseline.
- PostgreSQL database
- active deployment env file such as
.envorstack.env data/media
Create a PostgreSQL dump and copy the active env file.
Minimum expectation:
- database backup exists
- current image tags are known
- media library is preserved
- Stop the stack.
- Restore the active env file.
- Restore the PostgreSQL dump.
- Restore
data/mediaif needed. - Start the previously known-good image tags.
- Confirm:
- setup is not shown again
/api/system/readinessreturns expected service states/live?tab=controland/live?tab=statusshow the prior runtime state