<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Live Miracles</title>
    <description>The latest articles on DEV Community by Live Miracles (livemiracles).</description>
    <link>https://dev.to/livemiracles</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15494%2F57eb819c-a2d5-4a03-a789-040c16c9637e.png</url>
      <title>DEV Community: Live Miracles</title>
      <link>https://dev.to/livemiracles</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/livemiracles"/>
    <language>en</language>
    <item>
      <title>SRS, MediaMTX and Restreamer reject bonded SRT. Here is a relay that fixes it</title>
      <dc:creator>Nandisha</dc:creator>
      <pubDate>Sun, 01 Nov 2026 10:41:29 +0000</pubDate>
      <link>https://dev.to/livemiracles/srs-mediamtx-and-restreamer-reject-bonded-srt-here-is-a-relay-that-fixes-it-6fg</link>
      <guid>https://dev.to/livemiracles/srs-mediamtx-and-restreamer-reject-bonded-srt-here-is-a-relay-that-fixes-it-6fg</guid>
      <description>&lt;p&gt;SRT bonding sends one live stream over several network links at once, for example two uplinks or two SIM cards. If one link degrades or dies, the receiver still has the data from the other, so the stream does not drop. libsrt implements it as a &lt;em&gt;socket group&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The catch is the receiving end. A server has to accept the group, and most open source media servers cannot. We run &lt;a href="https://github.com/ossrs/srs" rel="noopener noreferrer"&gt;SRS&lt;/a&gt; in &lt;a href="https://github.com/live-miracles/bunryu" rel="noopener noreferrer"&gt;Bunryu&lt;/a&gt;, so we wrote a small relay, &lt;a href="https://github.com/live-miracles/goryu" rel="noopener noreferrer"&gt;Goryu&lt;/a&gt;, that accepts the bonded group and forwards ordinary SRT to the server behind it. This post shows how to put it in front of SRS, MediaMTX or any SRT server, and what we actually checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: terminate the group before the server
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fbonded-before-after.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fbonded-before-after.webp" alt="Sent directly, the bonded group is rejected by the server. Through Goryu, the legs are merged and the server receives one plain SRT stream" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Goryu listens on one port (i.e. &lt;code&gt;10081&lt;/code&gt;) with group connections enabled. libsrt merges the legs and drops the duplicate packets before Goryu reads anything, so Goryu sees one clean MPEG-TS stream. It automatically forwards that stream to your server's port (i.e. &lt;code&gt;10080&lt;/code&gt;) as a normal single-link SRT caller, with the same &lt;code&gt;streamid&lt;/code&gt;. The server never knows the input was bonded. An ordinary single-link SRT sender works on the same port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it with MediaMTX
&lt;/h2&gt;

&lt;p&gt;Start MediaMTX, then Goryu with a config that points at MediaMTX's SRT port (&lt;code&gt;8890&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; host bluenviron/mediamtx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"passphrase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; goryu.tar.gz https://github.com/live-miracles/goryu/releases/latest/download/goryu-linux-x86_64.tar.gz
&lt;span class="nb"&gt;mkdir &lt;/span&gt;goryu &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; goryu.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; goryu
&lt;span class="nv"&gt;LD_LIBRARY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;goryu/lib goryu/bin/goryu goryu.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now send a bonded stream to &lt;code&gt;srt://&amp;lt;host&amp;gt;:10081&lt;/code&gt; with &lt;code&gt;streamid=publish:stream&lt;/code&gt;, and play it back from MediaMTX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffplay &lt;span class="s1"&gt;'srt://127.0.0.1:8890?streamid=read:stream'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;streamid&lt;/code&gt; is the one thing to get right, because Goryu copies it unchanged to the server. These are the publish ids we used:&lt;/p&gt;

&lt;h2&gt;
  
  
  Passphrases
&lt;/h2&gt;

&lt;p&gt;SRT encrypts each hop on its own. By default Goryu uses one passphrase on both: set &lt;code&gt;passphrase&lt;/code&gt;. So similiarly to &lt;code&gt;streamid&lt;/code&gt; the encoder will use the same &lt;code&gt;passphrase&lt;/code&gt; as when directly connecting to the server. You can optionally give the server side its own &lt;code&gt;passphrase&lt;/code&gt; with &lt;code&gt;output_passphrase&lt;/code&gt;: an empty string for a server without a passphrase, or a different one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"passphrase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a-long-secret-for-encoders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_passphrase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Broadcast or main/backup
&lt;/h2&gt;

&lt;p&gt;libsrt 1.5 offers two group types, and the sender picks one. The receiver, Goryu, treats them the same: it merges what arrives and forwards one stream.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast&lt;/strong&gt; puts the whole stream on every link. If a link fails the others already have every packet, so nothing has to switch. The price is bandwidth: two links cost twice the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Main/backup&lt;/strong&gt; gives each link a weight. The heaviest link carries the stream and the others sit idle, sending only keepalives. If the main link becomes unstable the sender activates a standby. It costs one link's worth of data, which matters on metered cellular, but the switchover is its weak spot: packets sent while the main link was failing, before it was judged unstable, can be late or lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Goryu's status API every leg of a broadcast group is &lt;code&gt;running&lt;/code&gt;. In a main/backup group the standby is &lt;code&gt;idle&lt;/code&gt;, with a receive rate of about zero.&lt;/p&gt;

&lt;p&gt;We sent a main/backup group over loopback at 4 Mbps, with weights 10 and 5. The main leg was &lt;code&gt;running&lt;/code&gt; at 4.4 Mbps and the backup &lt;code&gt;idle&lt;/code&gt; at 0.000 Mbps. When we closed the main leg the backup became &lt;code&gt;running&lt;/code&gt;, about 2 MB arrived in the next four seconds (roughly 4 Mbps), and the downstream never reconnected.&lt;/p&gt;

&lt;p&gt;One catch: at a trickle (0.12 Mbps) libsrt kept both legs active, so a main/backup group needs real traffic before it settles on a main leg.&lt;/p&gt;

&lt;p&gt;Our hardwere encoder AJA Bridge Live supports only the &lt;strong&gt;Broadcast&lt;/strong&gt; mode, so we are using it with goryu. We have not tested &lt;strong&gt;Main/backup&lt;/strong&gt; mode with real hardware encoders yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we saw when a leg dropped
&lt;/h2&gt;

&lt;p&gt;We sent a stream through Goryu with two legs and closed the second leg nine seconds in, watching the status API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Legs&lt;/th&gt;
&lt;th&gt;Bytes forwarded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8 s&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;472,444&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;708,008&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;933,044&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The leg left the &lt;code&gt;legs[]&lt;/code&gt; list, the byte counter kept growing, and &lt;code&gt;outputConnected&lt;/code&gt; stayed &lt;code&gt;true&lt;/code&gt;. The server behind Goryu saw an unbroken stream. Each leg reports its own round-trip time, loss, retransmissions and buffer, which is what you want when you are deciding which uplink is the weak one.&lt;/p&gt;

&lt;h2&gt;
  
  
  With a real encoder, over the internet
&lt;/h2&gt;

&lt;p&gt;This is how we run it: Goryu in front of SRS (known as srt-bonding-relay until this month) has carried our live events for the past few months, a few days every week.&lt;/p&gt;

&lt;p&gt;We pointed an AJA BRIDGE LIVE at the Goryu relay (v3.3.0) on our Bunryu server, a Google Cloud VM in Mumbai, which forwards to the SRS on the same host. AJA lists SRT bonding over the public internet as a feature from firmware v1.16 (&lt;a href="https://www.aja.com/pdf/2024/AJA_BRIDGE-LIVE_ReleaseNotes_v1.16.pdf" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;), and it needs the Annual Maintenance Option. Four minutes after the relay started, its status API showed this for the two legs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Per leg&lt;/th&gt;
&lt;th&gt;Leg A&lt;/th&gt;
&lt;th&gt;Leg B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Round-trip time&lt;/td&gt;
&lt;td&gt;44.0 ms&lt;/td&gt;
&lt;td&gt;40.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive rate&lt;/td&gt;
&lt;td&gt;22.6 Mbps&lt;/td&gt;
&lt;td&gt;22.6 Mbps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packets received&lt;/td&gt;
&lt;td&gt;491,703&lt;/td&gt;
&lt;td&gt;491,737&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packets lost on this leg&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;157&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retransmissions&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dropped for arriving late&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive buffer&lt;/td&gt;
&lt;td&gt;242 ms&lt;/td&gt;
&lt;td&gt;239 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two full copies.&lt;/strong&gt; In broadcast mode each leg carries the whole stream, so both show about 22.6 Mbps. The two legs came from distinct public addresses, with 240 ms of latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real loss, handled.&lt;/strong&gt; Each leg lost a small share of its packets (about 0.01% and 0.03%) and asked for retransmissions, and nothing arrived too late. Goryu reports loss per leg, not for the merged stream, so this shows how each link behaved, not what the merged stream lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream.&lt;/strong&gt; Zero retry failures and no error on the stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The relay's journal covers five days. It records 18 bonded sessions and no plain ones, with no downstream retries and no failed connects. The longest session ran 21 hours 51 minutes and forwarded 215.8 GB, about 22 Mbps on average, and it ended only because the service was stopped for maintenance. These sessions are our regular use of the relay. The snapshot above and the cable pull were taken on a test feed, not during a live event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling one cable
&lt;/h2&gt;

&lt;p&gt;With the stream running, we unplugged one of the encoder's two LAN cables for about 45 seconds and plugged it back in, while recording the relay's status once a second. These are the samples around the event, on the VM's clock:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Legs&lt;/th&gt;
&lt;th&gt;Bytes forwarded&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14:22:37&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2,713,522,252&lt;/td&gt;
&lt;td&gt;both legs running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:22:41&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2,725,683,408&lt;/td&gt;
&lt;td&gt;one leg gone from &lt;code&gt;legs[]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:23:22&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2,837,954,000&lt;/td&gt;
&lt;td&gt;still one leg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:23:27&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2,852,886,652&lt;/td&gt;
&lt;td&gt;the leg is back, with no restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The stream never paused.&lt;/strong&gt; Between 14:22:37 and 14:23:27 the relay forwarded 139.4 MB in 50 seconds, which is 22.3 Mbps. In steady state before the pull it was 21.9 Mbps. Over the whole event &lt;code&gt;outputConnected&lt;/code&gt; stayed &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;retryFailures&lt;/code&gt; stayed 0 and no error was recorded, so SRS saw one unbroken connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The leg came back by itself.&lt;/strong&gt; Nobody touched Goryu or the encoder after the cable went back in. The status showed the leg absent for about 46 seconds, which matches the 45-second pull.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One oddity.&lt;/strong&gt; The rejoined leg reports 7,977 lost packets from the moment it appears. That is roughly the first 3.7 seconds of the stream at this bitrate, so it is probably the gap before the leg joined being counted as loss. We have not confirmed that, and the merged stream did not slow down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing in the log, in v3.3.0.&lt;/strong&gt; The journal has no line for the leg dropping or for it coming back. The status API shows it live, but afterwards only the counters remain. The next release, v3.4.0, logs both: one line per change with the leg's address and how many legs remain, such as &lt;code&gt;Bonded leg left streamid=... leg=&amp;lt;ip&amp;gt;:&amp;lt;port&amp;gt; legs=1&lt;/code&gt;, and &lt;code&gt;Bonded leg joined ...&lt;/code&gt; when it returns. A leg that flaps is limited to 12 lines per 30 seconds and then summarised, so a bad link cannot flood the journal. Bunryu writes the same changes to the pipeline's log.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bonding needs both ends.&lt;/strong&gt; The receiving server is as much a part of it as the encoder, and the open source ones mostly do not accept groups yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A relay is a small, replaceable piece.&lt;/strong&gt; Goryu is about 2,800 lines of C and a config of six fields (a seventh is optional), so you can read all of it before trusting it with a live stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the sender first.&lt;/strong&gt; The hard part today is finding something that sends a libsrt group, not receiving it. Of the hardware we know of, the AJA BRIDGE LIVE is the one we have seen do it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Goryu and its &lt;a href="https://live-miracles.github.io/projects/goryu/docs/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; are open source (MIT).&lt;/p&gt;

</description>
      <category>srt</category>
      <category>bonding</category>
      <category>goryu</category>
      <category>mediamtx</category>
    </item>
    <item>
      <title>50 inputs and 500 outputs in one dashboard</title>
      <dc:creator>Nandisha</dc:creator>
      <pubDate>Sun, 01 Nov 2026 10:33:40 +0000</pubDate>
      <link>https://dev.to/livemiracles/50-inputs-and-500-outputs-in-one-dashboard-5e2j</link>
      <guid>https://dev.to/livemiracles/50-inputs-and-500-outputs-in-one-dashboard-5e2j</guid>
      <description>&lt;p&gt;A long live event can mean dozens of inputs, each sent to a handful of destinations. Past a few streams, the question stops being "does it forward?" and becomes "can I still see what is happening without the screen turning into noise?"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/live-miracles/bunryu" rel="noopener noreferrer"&gt;Bunryu&lt;/a&gt; was tested with up to 50 inputs and 500 outputs on one server. Here is what the dashboard showed and how the control plane performed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we tested
&lt;/h2&gt;

&lt;p&gt;We started the isolated test stack that ships with the repo, created 50 pipelines, and gave each one 10 outputs: 500 in total, the supported maximum. Every pipeline had a live input: a looped 720p clip published over RTMP. All 500 outputs were running.&lt;/p&gt;

&lt;p&gt;One thing to know before looking at the pictures: &lt;strong&gt;the outputs are simulated.&lt;/strong&gt; Each one is a stand-in for FFmpeg that reports progress and sends nothing over the network. The control plane, SRS, the inputs and the dashboard are real, so this shows what the dashboard looks like and what it costs the control plane. It does not show FFmpeg's own CPU and RAM, which our &lt;a href="https://live-miracles.github.io/projects/bunryu/docs/getting-started/sizing/" rel="noopener noreferrer"&gt;sizing page&lt;/a&gt; covers separately. The machine was an 8-core laptop shared with other work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Foverview-500-outputs.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Foverview-500-outputs.webp" alt="The overview with 50 inputs and 500 outputs: host charts, totals, and pipelines grouped in the sidebar" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The left column lists every pipeline, organized into groups (here Main Stage, Halls, Studios, Chapels and Plaza). Each row has a status dot, its output count, its name, how long it has been up and its input protocol. The totals at the top say "50 inputs, 500 outputs" with a green badge for each. To the right are the host's CPU, RAM and network charts and a table of what each process costs.&lt;/p&gt;

&lt;p&gt;Below that are two lists: every input, then every output.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something goes wrong
&lt;/h2&gt;

&lt;p&gt;The useful test of a big dashboard is whether a problem shows up without hunting for it. We took two of the 50 inputs offline and watched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Ftwo-inputs-down.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Ftwo-inputs-down.webp" alt="Two inputs drop: the totals change, the affected pipelines turn yellow, and the Issues tab narrows the list" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Within a few seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The totals changed to 48 live inputs and 480 healthy outputs, with a yellow 20 for the outputs that belong to the two dead inputs.&lt;/li&gt;
&lt;li&gt;The two pipelines turned yellow in their groups in the sidebar.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Issues&lt;/strong&gt; tab narrowed the output list to those 20 outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fissues-tab.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fissues-tab.webp" alt="The Issues tab lists the 20 affected outputs, each with its reason" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each row says why: "Output is running but input is not live." The outputs were not failing; they were waiting for an input that was no longer live, and that is what the dashboard told us.&lt;/p&gt;

&lt;h2&gt;
  
  
  One pipeline at a time
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fpipeline-view.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fpipeline-view.webp" alt="A pipeline's own view: its input, publish URLs and its 10 outputs" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opening a pipeline shows its input, its publish URLs and its 10 outputs on one screen, each with status, uptime, bitrate and memory. A group of ten outputs fits without scrolling, which is the level you work at when you are adjusting one destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control plane with one dashboard open&lt;/td&gt;
&lt;td&gt;about 17% of one core, 251 MB of RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/api/health&lt;/code&gt;, fetched every 5 s by each dashboard&lt;/td&gt;
&lt;td&gt;296 KB, 18 KB gzipped, about 30 ms to produce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard first render (headless Chrome, locally)&lt;/td&gt;
&lt;td&gt;0.7 to 0.8 s (one of four runs took 1.3 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overview page length&lt;/td&gt;
&lt;td&gt;about 28,000 px, roughly 32 screens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the control plane's costs. A real 500-output server also runs 500 FFmpeg processes. Our sizing page puts that at about 30 vCPUs and 17 to 30 GB of RAM at 8 Mbps per output.&lt;/p&gt;

</description>
      <category>bunryu</category>
      <category>dashboard</category>
      <category>scale</category>
    </item>
    <item>
      <title>One faulty audio embedder disrupted a mega event</title>
      <dc:creator>Nandisha</dc:creator>
      <pubDate>Sun, 01 Nov 2026 10:31:00 +0000</pubDate>
      <link>https://dev.to/livemiracles/one-faulty-audio-embedder-disrupted-a-mega-event-4iha</link>
      <guid>https://dev.to/livemiracles/one-faulty-audio-embedder-disrupted-a-mega-event-4iha</guid>
      <description>&lt;p&gt;Both audio meters were moving. The dry run had gone well. We had two fiber paths from the event to the streaming server. Yet when the event went live, viewers across more than 100 streams started reporting poor-quality audio that kept breaking up.&lt;/p&gt;

&lt;p&gt;Switching to the backup feed did not help. Switching to a third feed finally did, about an hour into the live event. By then, many viewers had left.&lt;/p&gt;

&lt;p&gt;This is how one suspected faulty audio embedder affected all those streams, why our monitoring missed it, and what we changed in &lt;a href="https://github.com/live-miracles/live-gallery" rel="noopener noreferrer"&gt;Live Gallery&lt;/a&gt; afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our monitoring heard only one channel
&lt;/h2&gt;

&lt;p&gt;We were monitoring the streams in &lt;a href="https://github.com/live-miracles/live-gallery" rel="noopener noreferrer"&gt;Live Gallery&lt;/a&gt; with a mono speaker setup that, in our arrangement, let us hear only the left channel. We could also see separate volume bars for left and right. Both showed audio, and what we heard sounded acceptable, so we thought the feeds were healthy.&lt;/p&gt;

&lt;p&gt;But listening to the left channel alone is different from listening to a mono mix of left and right. We had checked that audio was present in each channel. We had not checked whether the channels worked together.&lt;/p&gt;

&lt;p&gt;That distinction became painful during the event. Some phones played the audio acceptably; others sounded bad or lost much of it. Headphones could sound fine too. At the time, that inconsistency made the reports harder to explain. In retrospect, it was a useful clue: the result depended on how the playback device handled the two channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two active meters can hide bad audio
&lt;/h2&gt;

&lt;p&gt;When two channels carry similar sound with opposite polarity, each channel can sound fine on its own. Their volume meters can look normal too. Trouble appears when a playback system adds them together.&lt;/p&gt;

&lt;p&gt;A mono mix commonly combines the channels as &lt;code&gt;(L + R) / 2&lt;/code&gt;. If the right channel is an inverted copy of the left, the positive parts of one meet the negative parts of the other, and the sound cancels. With partial cancellation, voices can become thin, quiet, or unstable rather than disappearing completely.&lt;/p&gt;

&lt;p&gt;Stereo playback can keep the channels separate, so headphones may conceal the problem. Devices that combine the channels can expose it.&lt;/p&gt;

&lt;p&gt;Analysis of the recording later showed negative correlation between the channels and a loss of level when they were combined. The strength varied through the affected portion. That points to a stereo compatibility problem; it does not, by itself, tell us which physical channel or connection was at fault.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fphase-mismatch-recording.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fphase-mismatch-recording.webp" alt="A zoom from the affected recording shows blue left-channel and red right-channel waveforms moving in opposite directions, with correlation rounded to -1.000." width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This 33.3-millisecond sample from just before the feed switch shows the problem clearly: when one channel rises, the other falls. Cancellation was not equally strong throughout the incident.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fphase-in-sync-recording.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fphase-in-sync-recording.webp" alt="A 33.3-millisecond zoom around 01:20:00 in the recording shows the blue left-channel and red right-channel waveforms closely following each other, with correlation rounded to +0.999." width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For comparison, this sample after the switch to the clean feed shows the channels rising and falling together. Their correlation is close to +1. The key difference is their shared direction, not the height of the waveform: the two images show different moments of audio. Healthy stereo does not need to match this closely, but these channels reinforce rather than cancel each other when combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backup had the same upstream fault
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fshared-embedder-paths.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Fshared-embedder-paths.webp" alt="The video and audio mixers feed a shared suspected faulty embedder supplying both fiber paths to the streaming server. A separate embedding path supplies the clean feed that restored audio." width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our first response was to switch fiber paths. We had two paths from the venue to the streaming server, but both carried audio from the same suspected faulty embedder. Changing the transport path could not remove a problem already present in the source.&lt;/p&gt;

&lt;p&gt;The third feed was different. It took video from the video mixer and audio from the audio mixer through a separate embedding path. We normally used this clean feed to make social media shorts because it did not include the event overlays and lower thirds.&lt;/p&gt;

&lt;p&gt;Switching to that feed cleared the audio problem about an hour after the live broadcast started. It also exposed the gap in our backup plan: the two fiber paths were separate, but a critical part of their source chain was shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we added to Live Gallery
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/live-miracles/live-gallery" rel="noopener noreferrer"&gt;Live Gallery&lt;/a&gt; is our open-source application for monitoring many live streams simultaneously. After this incident, we added an audio phase mismatch alert and a choice of mono or stereo listening mode.&lt;/p&gt;

&lt;p&gt;The phase check looks at the relationship between left and right, including their correlation and how much level is lost when they are combined. It evaluates sustained evidence across multiple samples rather than treating one negative reading as a fault. We want to avoid nuisance warnings: normal stereo material can contain differences between channels, and negative correlation alone is not proof of broken equipment.&lt;/p&gt;

&lt;p&gt;The mono listening option combines left and right so we can hear cancellation during a check. It closes the specific gap in our earlier setup, where hearing only the left channel gave us confidence that the full stereo feed was safe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Flive-gallery-phase-warning-video.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Flive-miracles.github.io%2Fblog%2Fimages%2Flive-gallery-phase-warning-video.webp" alt="Live Gallery 1.6.1 showing event footage in a local-file tile, with an Audio phase mismatch warning over the video, active audio meters, and one alert in the toolbar." width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our screenshot of Live Gallery 1.6.1 shows the phase warning directly over the playing recording, even while the channel meters show audio activity. This demonstrates the monitoring added after the incident, not an alert available during the original event.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these changes cannot guarantee
&lt;/h2&gt;

&lt;p&gt;The alert is a warning about possible cancellation, not a diagnosis of an embedder. It cannot identify a loose cable, determine which channel was inverted, or promise that every viewer's device will sound good. Quiet passages, intermittent faults, and less pronounced cancellation may also escape detection. We prefer some missed detections to constant false alarms that operators learn to ignore.&lt;/p&gt;

&lt;p&gt;Mono listening and phase warnings give us additional ways to notice this failure. They do not replace listening, investigating early hardware trouble, or checking which components our backup feeds share.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it took so long to fix
&lt;/h2&gt;

&lt;p&gt;The first reports of bad audio were confusing because the feed sounded fine on our monitoring setup. We only confirmed that something was wrong after checking the streams on our own phones.&lt;/p&gt;

&lt;p&gt;Even after confirming the issue, we did not understand its cause. In all our years of regularly running live events, this was the first time we had encountered this particular phase mismatch problem. We initially suspected a fault in our encoder or other hardware, especially because switching to the backup feed had not helped.&lt;/p&gt;

&lt;p&gt;Only later did we consider trying the clean feed. Switching to it would mean the editors would lose their feed, and viewers would experience a few seconds of buffering. We were not even sure it would fix the issue. Ultimately, though, it was the right choice.&lt;/p&gt;

&lt;p&gt;Using only the left channel in the streaming encoder would also have fixed the cancellation problem. At the time, however, we did not yet understand the root cause, so we tried the clean feed first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways for the next event
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the feed in stereo and in a combined mono mix during the dry run. Left-only listening is a separate check.&lt;/li&gt;
&lt;li&gt;Trace backup feeds all the way upstream. Two transport paths can still share one faulty source device.&lt;/li&gt;
&lt;li&gt;Have a mature monitoring system that can detect various issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clean feed saved this event's remaining broadcast. The more lasting change was learning to test what our viewers might actually hear, including what happens when two apparently healthy channels become one.&lt;/p&gt;

</description>
      <category>audio</category>
      <category>streaming</category>
      <category>reliability</category>
      <category>livegallery</category>
    </item>
    <item>
      <title>One leaking FFmpeg process took down six live streams</title>
      <dc:creator>Nandisha</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:10:13 +0000</pubDate>
      <link>https://dev.to/livemiracles/one-leaking-ffmpeg-process-took-down-six-live-streams-3kpp</link>
      <guid>https://dev.to/livemiracles/one-leaking-ffmpeg-process-took-down-six-live-streams-3kpp</guid>
      <description>&lt;p&gt;A live event cannot be restarted. When we built &lt;a href="https://github.com/live-miracles/bunryu" rel="noopener noreferrer"&gt;Bunryu&lt;/a&gt;, aself-hosted server that forwards one live stream to many destinations, we assumed things would go wrong and tried to keep each failure small. In July 2026 a single FFmpeg process showed us where that was not true yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Someone looked at the dashboard and asked: "Output 1 has an error, output 2 doesn't. Why?"&lt;/p&gt;

&lt;p&gt;Output 1's last error was &lt;code&gt;exit=SIGKILL&lt;/code&gt;. That is not an application error, so we went to the host logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;Six streams were running on one VM, with three pipelines and six outputs. This is the chain we pieced together from the logs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All six encoders dropped and reconnected repeatedly for about eleven minutes.&lt;/li&gt;
&lt;li&gt;When the streams settled, every output's FFmpeg process restarted and resynchronised. The audio at the reconnect boundary was corrupted.&lt;/li&gt;
&lt;li&gt;FFmpeg's AAC decoder misread the corrupted audio as a bogus 22.2-channel layout and leaked memory instead of failing cleanly.&lt;/li&gt;
&lt;li&gt;One process grew to about 1.6 GB and the kernel's OOM killer killed it.&lt;/li&gt;
&lt;li&gt;FFmpeg children share a cgroup with the control-plane service, so systemd saw the whole service as failed. It restarted the service, and the restart cascaded to SRS and the SRT relay.&lt;/li&gt;
&lt;li&gt;All six encoders reconnected within the same two seconds, which recreated the corrupt-resync conditions.&lt;/li&gt;
&lt;li&gt;Fourteen minutes later a second output hit the same bug and was killed. That second kill was the error on the dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Output 2 had no error only because it was still on its way up. When we looked, it was at 644 MB and&lt;br&gt;
climbing. "No error" did not mean "healthy".&lt;/p&gt;

&lt;p&gt;The root cause was a loop: flaky input, a decoder leak on resync, an OOM kill, a restart of the whole&lt;br&gt;
stack, and a fresh mass reconnect. Isolation between outputs did not help, because the process&lt;br&gt;
topology made the kernel kill a shared fate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpajqxbwa8oxcvz435jw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpajqxbwa8oxcvz435jw.webp" alt="The restart loop in five steps, with the memory watchdog we added between the decoder leak and the OOM kill" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Drop damaged packets before they reach the decoder.&lt;/strong&gt; Outputs now run with&lt;br&gt;
&lt;code&gt;-fflags +discardcorrupt -err_detect crccheck+bitstream&lt;/code&gt;. This reduces the chance of the misparse, but it does not remove it, so we did not stop there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A memory watchdog that acts first.&lt;/strong&gt; Every five seconds the app checks each output's resident memory. A healthy copy output sits at about 65 to 90 MB. If one crosses 200 MB, we kill it, record why, and let the normal retry loop start a fresh one. 200 MB is far below the 1.5 GB where the kernel steps in, so the kernel never needs to, and the cascade never starts. Transcoding outputs have higher limits because they legitimately use more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries that don't pile up.&lt;/strong&gt; An output retries indefinitely, waiting 1, 2, 4, 8, then 16 seconds after repeated quick failures. A run that stayed up for a minute resets the sequence, so a drop late in a long event is retried after about a second. Each wait is shortened by up to a quarter at random, so outputs that fail together do not respawn in lockstep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence on every failure.&lt;/strong&gt; Each output keeps an error history with the exit status, the reason and the tail of FFmpeg's stderr. That history, together with the host logs, is how we could explain a &lt;code&gt;SIGKILL&lt;/code&gt; after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did not fix
&lt;/h2&gt;

&lt;p&gt;The shared cgroup is still there. The watchdog keeps the kernel from ever triggering the cascade, but if a process grew faster than the watchdog checks, or something else ran the host out of memory, the same chain could start again. We would rather say that than claim the cascade is gone.&lt;/p&gt;

&lt;p&gt;We also test these paths on purpose: a destination that freezes, one that accepts a connection and never reads, one that refuses connections, a CPU-saturated host, and a soak run with 50 healthy outputs. No test makes a server perfect. The goal is that when something fails it stays small, fixes itself where that is safe, and leaves enough evidence to explain it afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check how failures propagate, not only whether components are isolated.&lt;/strong&gt; Separate processes still shared a cgroup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put your own watchdog below the kernel's threshold.&lt;/strong&gt; An OOM kill is the last resort, not the plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"No error" is not "healthy".&lt;/strong&gt; Look at resource trends, not only error flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down the timeline.&lt;/strong&gt; A real incident is the best test case you will ever get.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project and its &lt;a href="https://live-miracles.github.io/projects/bunryu/docs/operations/reliability/" rel="noopener noreferrer"&gt;reliability documentation&lt;/a&gt; are open source.&lt;/p&gt;

</description>
      <category>bunryu</category>
      <category>reliability</category>
      <category>ffmpeg</category>
      <category>srt</category>
    </item>
  </channel>
</rss>
