<?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: RUSEGAL</title>
    <description>The latest articles on DEV Community by RUSEGAL (@rusegal).</description>
    <link>https://dev.to/rusegal</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%2Fuser%2Fprofile_image%2F4067223%2F339c17da-fcaf-48fd-85fb-9aad846c90fe.jpg</url>
      <title>DEV Community: RUSEGAL</title>
      <link>https://dev.to/rusegal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rusegal"/>
    <language>en</language>
    <item>
      <title>RUSEON Core – Self-hosted video infrastructure for RTSP and AI</title>
      <dc:creator>RUSEGAL</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:39:40 +0000</pubDate>
      <link>https://dev.to/rusegal/ruseon-core-self-hosted-video-infrastructure-for-rtsp-and-ai-jhf</link>
      <guid>https://dev.to/rusegal/ruseon-core-self-hosted-video-infrastructure-for-rtsp-and-ai-jhf</guid>
      <description>&lt;p&gt;I built RUSEON Core — a self-hosted server for working with RTSP cameras, video streaming, archive recording, and connecting AI pipelines.&lt;br&gt;
The project came out of a work-related task. We needed to stream surveillance cameras to the website of a government organization. The cameras themselves are portable: they are installed with IoT routers and constantly move between different sites. Therefore, a traditional NVR at each site was not suitable — the video infrastructure had to be centralized. The task was simple: receive RTSP and deliver it as HLS (or any format that can be played in a browser without problems). We also needed 24/7 recording with at least two weeks of storage.&lt;/p&gt;

&lt;p&gt;For the first three years — Wowza, everything worked well, but with huge resource consumption. Then — Flussonic, which was better, but even now 60 permanently active cameras and 15–20 occasionally active cameras consume around 10 GB of RAM.&lt;br&gt;
At first I looked at MediaMTX, which seemed to be as close as possible to what I needed. But around the streaming layer, there were still many other things needed — UI, archive handling, camera-specific data, monitoring, AI integration.&lt;/p&gt;

&lt;p&gt;Instead of forking MediaMTX, I took gortsplib as the foundation and started building my own system on top of it. The pipeline looks roughly like this: RTSP – RUSEON Core – WebRTC/HLS/Recording – gRPC – AI worker – AI Metadata. At the moment there is a web UI, Prometheus metrics, RBAC, camera tags and folders, camera history, traffic accounting, fragmented archive with timeshift, MQTT and gRPC integration.&lt;/p&gt;

&lt;p&gt;For WebRTC/HLS, no unnecessary work is done. In 90% of cases, the Zero-Alloc and Zero-Transcoding principles are followed. In particular, one of the features is that when a codec is not supported, WebCodecs is used on the client side. As well as AI detection through ONNX Runtime in the browser (while still retaining the option of using traditional AI).&lt;/p&gt;

&lt;p&gt;Some things in the implementation also turned out to be quite interesting, for example using the OS page cache so that the archive does not have to be kept in memory, and implementing batching through sendmmsg() for WebRTC, which reduced CPU load by about 27% in my tests. Speaking of tests, during an 8-hour test with 600 incoming RTSP streams, 18,000+ incoming FPS, 1,800 HLS viewers, 240 WebRTC peers, 10 API workers, 20 gRPC AI workers, and an event bus — average memory consumption was 707 MB, average CPU usage was 52.4%, with a peak of 98.1%. On average, about 3.2 cores out of 12 were used. (12 cores, 32 GB RAM)&lt;br&gt;
An important clarification — the load generator and RUSEON Core were running on the same machine. This is not a distributed benchmark. Logically, the numbers should be better without competition for resources.&lt;/p&gt;

&lt;p&gt;I don't consider the project finished yet. I especially want to understand whether the architecture itself makes sense and whether I am trying to solve too many things with a single server. I would be especially interested in feedback from people who have worked with RTSP/media servers, WebRTC, NVRs, or computer vision pipelines.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/RUSEGAL/ruseon-core" rel="noopener noreferrer"&gt;https://github.com/RUSEGAL/ruseon-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Scaling WebRTC Streams with Engine Pooling in Go</title>
      <dc:creator>RUSEGAL</dc:creator>
      <pubDate>Tue, 18 Aug 2026 06:01:36 +0000</pubDate>
      <link>https://dev.to/rusegal/scaling-webrtc-streams-with-engine-pooling-in-go-3a72</link>
      <guid>https://dev.to/rusegal/scaling-webrtc-streams-with-engine-pooling-in-go-3a72</guid>
      <description>&lt;h2&gt;
  
  
  FATAL: crypto/rand: blocked on getrandom() syscall, or 100% CPU at the 25th Client
&lt;/h2&gt;

&lt;p&gt;The title is catchy enough—now let’s get to the point.&lt;/p&gt;

&lt;p&gt;This error is a classic side effect of the default Pion WebRTC setup in Go. It is easy to reproduce: you read the usual Pion usage guides, deploy a nice-looking WHEP handler, open a couple of—okay, okay, not literally a couple, just a figure of speech—browser tabs with the player, and suddenly the server starts choking.&lt;/p&gt;

&lt;p&gt;Handshakes begin taking seconds, ICE times out, and half of the pprof flame graph is filled with &lt;code&gt;crypto/elliptic.p256OrdSqr&lt;/code&gt; and map allocations somewhere inside the engine.&lt;/p&gt;

&lt;p&gt;Surprised?&lt;/p&gt;

&lt;p&gt;There is really nothing surprising about it if you read most WebRTC tutorials for Go a little more carefully. I get the impression that many of them were written by people who never load-tested their implementation with even fifty concurrent viewers—at most a couple of streams, and that was apparently enough for them.&lt;/p&gt;

&lt;p&gt;Here is what they usually do: for every POST request containing an SDP offer, they create a new &lt;code&gt;webrtc.NewAPI()&lt;/code&gt;, register the default codecs, call &lt;code&gt;api.NewPeerConnection()&lt;/code&gt;, and happily return the response.&lt;/p&gt;

&lt;p&gt;On localhost, with a couple of clients, it flies.&lt;/p&gt;

&lt;p&gt;In production, however, it turns into a disaster.&lt;/p&gt;

&lt;p&gt;The problem here is not WebRTC itself—and certainly not Pion, which is a great library—and it is not Go either.&lt;/p&gt;

&lt;p&gt;The problem is that expensive global “infrastructure” is being created for every single request instead of simply being reused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s dig into what is actually happening
&lt;/h2&gt;

&lt;p&gt;A small disclaimer: from this point on, I am not going to explain every single term in detail. If something is unclear, it is better to check the documentation or ask in the comments—otherwise, this would turn into an extremely long post.&lt;/p&gt;

&lt;p&gt;The first expensive operation is generating cryptographic material for DTLS.&lt;/p&gt;

&lt;p&gt;For this, ECDSA P-256 keys are generated and later used to create the DTLS certificate. It is important to understand that key generation is not just a couple of constructor calls and writing a few bytes into memory.&lt;/p&gt;

&lt;p&gt;Under the hood, it uses &lt;code&gt;crypto/ecdsa&lt;/code&gt;, heavy elliptic-curve mathematics, and cryptographically secure randomness from &lt;code&gt;crypto/rand&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second expensive operation is allocating hundreds of small structures on the heap for all possible codecs, RTCP interceptors, RTP headers, header extensions, and other internal components.&lt;/p&gt;

&lt;p&gt;None of these allocations are exactly free.&lt;/p&gt;

&lt;p&gt;The third expensive operation happens at the end: the implementation goes into the OS and opens a bunch of random UDP ports for ICE candidate gathering and binding.&lt;/p&gt;

&lt;p&gt;In a typical configuration, this means allocating new network resources for every &lt;code&gt;PeerConnection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, for every new connection, the pipeline looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New viewer

    │

    ├── ECDSA P-256
    │   └── crypto/ecdsa + crypto/rand
    │
    ├── Creating and initializing WebRTC structures
    │   └── hundreds of small heap allocations
    │
    ├── Registering codecs and interceptors
    │
    ├── Creating the network transport
    │
    └── UDP sockets + ICE candidate gathering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And what is the result?&lt;/p&gt;

&lt;p&gt;Thirty viewers connect simultaneously—the CPU gets hammered by key generation, the connection tracking table starts ballooning, and the garbage collector strikes a pose while trying to clean up tons of dead objects left behind after every closed connection.&lt;/p&gt;

&lt;p&gt;There is no point in building workarounds here.&lt;/p&gt;

&lt;p&gt;The architecture needs to be designed properly from the beginning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;everything that can be reused should be reused.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution we used
&lt;/h2&gt;

&lt;p&gt;In RUSEON Core, all of the heavy work was moved into an isolated singleton engine in &lt;code&gt;internal/webrtc/engine.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Certificate generation now happens exactly once through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ecdsa&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GenerateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elliptic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;P256&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when the server starts.&lt;/p&gt;

&lt;p&gt;The browser does not care whether the certificate was created a second ago or has been sitting in RAM for months. All it needs is a cryptographically valid fingerprint in the SDP offer—and nothing more.&lt;/p&gt;

&lt;p&gt;The codec registry is initialized at startup as well and no longer constantly touches the heap.&lt;/p&gt;

&lt;p&gt;The network layer is also usually a bottleneck.&lt;/p&gt;

&lt;p&gt;But instead of opening dozens of random ports, we attached &lt;code&gt;webrtc.NewICEUDPMux&lt;/code&gt; to a single UDP socket.&lt;/p&gt;

&lt;p&gt;One port is used by the entire system.&lt;/p&gt;

&lt;p&gt;The OS kernel multiplexes all incoming STUN, DTLS, and SRTP traffic through a single file descriptor, while Pion distributes packets between ICE and WebRTC sessions in userspace.&lt;/p&gt;

&lt;p&gt;No port exhaustion.&lt;/p&gt;

&lt;p&gt;No need to dance around opening a &lt;code&gt;50000–60000&lt;/code&gt; port range in the firewall.&lt;/p&gt;

&lt;p&gt;On top of that, an engine pool is used through &lt;code&gt;sync.Pool&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;WebRTCEnginePool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;newWebRTCEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: &lt;code&gt;sync.Pool&lt;/code&gt; is not used here as guaranteed storage for a predefined set of objects.&lt;/p&gt;

&lt;p&gt;Its purpose is to allow temporary engine objects to be reused and reduce unnecessary allocations on the hot path. If the runtime clears the pool, &lt;code&gt;New&lt;/code&gt; simply creates a new instance.&lt;/p&gt;

&lt;p&gt;When a WHEP request arrives in &lt;code&gt;internal/api/handler.go&lt;/code&gt;, the handler does not create anything unnecessary.&lt;/p&gt;

&lt;p&gt;It retrieves an already warmed-up and ready instance, feeds it the pre-generated certificate through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;baseConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;webrtc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Certificate&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and immediately returns the SDP response.&lt;/p&gt;

&lt;p&gt;In practice, the time from request to response is reduced to just a couple of system calls—not counting, of course, the creation of the &lt;code&gt;PeerConnection&lt;/code&gt; itself, DTLS/ICE configuration, and SDP negotiation.&lt;/p&gt;

&lt;p&gt;The end result is that a new viewer no longer forces the entire WebRTC stack to be created from scratch.&lt;/p&gt;

&lt;p&gt;It creates only its own connection-specific state on top of an already prepared engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls when using the H.264 codec
&lt;/h2&gt;

&lt;p&gt;If you simply take NALU packets from the ring buffer and feed them directly into the stream, the video on the client will constantly freeze.&lt;/p&gt;

&lt;p&gt;In our case, the browser’s hardware decoder requires clean Annex B with proper &lt;code&gt;0x00 0x00 0x00 0x01&lt;/code&gt; delimiters, and every key IDR frame must include the stream’s SPS and PPS parameters.&lt;/p&gt;

&lt;p&gt;One of the solutions we used was to allocate a pre-sized 100 KB byte buffer in &lt;code&gt;internal/webrtc/muxer.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This makes it possible to reliably avoid slice reallocations while concatenating headers at 30 FPS.&lt;/p&gt;

&lt;p&gt;Why does this matter at all?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;append()&lt;/code&gt; on the hot path can be a scary thing.&lt;/p&gt;

&lt;p&gt;As long as there is enough capacity, everything is fine.&lt;/p&gt;

&lt;p&gt;But once there is not, Go allocates a new array, copies the old data into it, and the old array eventually ends up as dinner for the garbage collector.&lt;/p&gt;

&lt;p&gt;At 30 frames per second, this can mean dozens of additional allocations for every stream.&lt;/p&gt;

&lt;p&gt;And you never really know when the accumulated garbage will become significant enough to trigger the collector.&lt;/p&gt;

&lt;p&gt;Multiply that by the number of streams and viewers, and I think the outcome is obvious.&lt;/p&gt;

&lt;p&gt;The whole idea comes down to this: if the approximate size of a working buffer is known and bounded, allocate it once and reuse it.&lt;/p&gt;

&lt;p&gt;Do not allow a slice to grow dynamically at every stage of the hot pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;For testing, we used our own load-testing utility, &lt;code&gt;cmd/loadtest/main.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It simultaneously ran:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;50 synthetic cameras&lt;/li&gt;
&lt;li&gt;the REST API&lt;/li&gt;
&lt;li&gt;30 HLS clients&lt;/li&gt;
&lt;li&gt;30 active WebRTC viewers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[WebRTC WHEP] Sessions OK: 30 (err: 0) | RTP Packets: 488 214 | Egress: 9.04 MB/s

Handshake Latency: p50=3.21ms | p95=6.84ms | max=11.20ms

HeapAlloc: 34MB | GC Pause Total: 3.82ms | Goroutines: 142
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost half a million RTP packets, a stable 9 MB/s of outgoing traffic, and exactly zero negotiation errors.&lt;/p&gt;

&lt;p&gt;Handshake latency dropped from &lt;strong&gt;800+ ms to 3 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The main takeaway from this article is simple:&lt;/p&gt;

&lt;p&gt;If WebRTC is not “handling” real video workloads, do not blindly trust the tutorials.&lt;/p&gt;

&lt;p&gt;Especially when those tutorials suggest solving such problems by pulling in heavy CGO bindings to C++.&lt;/p&gt;

&lt;p&gt;Our experience shows that the bottleneck is not the runtime itself, but rather mindlessly regenerating cryptography for every little thing and constantly hitting the allocator on the hot path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebRTC is not slow. The architecture around it is slow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/RUSEGAL/ruseon-core" rel="noopener noreferrer"&gt;https://github.com/RUSEGAL/ruseon-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>HLS Lazy Muxing 2.0: How to Cut CPU Usage by 80% Without Stopping Archive Recording</title>
      <dc:creator>RUSEGAL</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:55:41 +0000</pubDate>
      <link>https://dev.to/rusegal/hls-lazy-muxing-20-how-to-cut-cpu-usage-by-80-without-stopping-archive-recording-2jda</link>
      <guid>https://dev.to/rusegal/hls-lazy-muxing-20-how-to-cut-cpu-usage-by-80-without-stopping-archive-recording-2jda</guid>
      <description>&lt;p&gt;If anyone has ever worked with video streaming over the internet—i.e., used a classic media server operating on an RTSP-to-HLS model (or similar protocols)—then they know the kinds of problems that arise. As an example, let’s take two industry giants: Flussonic and Wowza. Flussonic’s documentation provides clear hardware specifications at which CPU usage will hit 100%: 250 cameras at a bitrate of 2 Mbps on a Xeon E3-1230v5 3.4 GHz with 32 GB of RAM. Wowza does not provide equally specific examples, but if we extrapolate a bit, roughly 350–400 cameras at 2 Mbps on the same hardware would also max out the CPU.&lt;/p&gt;

&lt;p&gt;Let me add one caveat: I understand that this hardware is fairly old. But around 80% of small and medium-sized businesses still run on something in this range—at least those that do not use cloud infrastructure or rent hardware in a data center. So, at first glance, you might think: well, that’s just the workload, what can you do? The problem is that, at any given moment, nobody may actually be watching these streams. The hypothetical security guard is scrolling through their phone, drinking coffee—and meanwhile, the server continues 24/7 to package raw frames into HLS segments and write the archive, heating up the server room.&lt;/p&gt;

&lt;p&gt;It is simply a waste of resources when they are not actually needed. And in an era where hardware costs money, these things are worth paying attention to. &lt;/p&gt;

&lt;p&gt;The solution seems obvious, and many people are already familiar with it: &lt;strong&gt;On Demand&lt;/strong&gt; mode. No viewers—shut down the camera. A viewer appears—serve the HLS stream. At first glance, everything seems perfect. Problem solved, everyone can go home.&lt;/p&gt;

&lt;p&gt;But this is where the main problem with classic On Demand appears: as soon as there are no viewers and the camera is shut down, archive recording stops as well. For any security system, that is a major red flag.&lt;/p&gt;

&lt;p&gt;While developing RUSEON Core, I spent a long time wrestling with this architectural trade-off, because the main goal of the project is to consume as few resources as possible while delivering the maximum benefit. Eventually, I came up with a feature that I call &lt;strong&gt;Lazy Muxing 2.0&lt;/strong&gt; (sounds pretty good, doesn’t it?).&lt;/p&gt;

&lt;p&gt;The idea is simple: the core runs continuously, the archive is always recorded, and only the heaviest component—the HLS muxer—goes to sleep. Now, let me explain how this was implemented and what pitfalls can be found along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Low-Copy approach
&lt;/h2&gt;

&lt;p&gt;A traditional video server pipeline is essentially monolithic. But as I mentioned in previous articles, RUSEON is architecturally split into independent components. These are not microservices, nor is this an attempt to move everything into separate processes. The point is specifically to isolate modules within a single process—meaning that no module should drag another one along with it, much less affect its lifecycle.&lt;/p&gt;

&lt;p&gt;If we look under the hood (&lt;code&gt;ring.go&lt;/code&gt;), we can see an isolated goroutine continuously pulling the RTSP stream. Frames are placed into a ring buffer, and from that point onward a low-copy approach is used.&lt;/p&gt;

&lt;p&gt;Disk recording (&lt;code&gt;recorder.go&lt;/code&gt;) is simply a subscriber to that buffer. It reads pointers to memory and writes the bytes to disk. That’s it. It does not care in the slightest whether anyone is watching or not. The archive is recorded 24/7.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/recorder/recorder.go (simplified)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Recorder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Subscribe to the RingBuffer (Data Plane)&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ringBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Read a pointer to the frame without copying the actual bytes (Low-Copy)&lt;/span&gt;
        &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// Write NAL unit bytes to disk&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeToDisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The heavy HLS muxer (&lt;code&gt;muxer.go&lt;/code&gt;), on the other hand, is not a mandatory part of the pipeline. It simply sits there like a “lazy plugin” and attaches to the same buffer only when there is an actual consumer.&lt;/p&gt;

&lt;p&gt;No HTTP requests for the playlist? The muxer does not even exist in memory.&lt;/p&gt;

&lt;p&gt;This creates a simple principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Archive is permanent. HLS is temporary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This separation makes it possible to avoid wasting server resources on creating and running an HLS stream for a camera that nobody is currently watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how is this different from what Flussonic does?
&lt;/h2&gt;

&lt;p&gt;A fair question arises here: is this actually something fundamentally new? No.&lt;/p&gt;

&lt;p&gt;For example, Flussonic has long implemented separation between stream ingestion, DVR, and delivery.&lt;/p&gt;

&lt;p&gt;In their documentation, Flussonic describes its approach as &lt;strong&gt;just-in-time packaging&lt;/strong&gt;: the server receives the stream once and can immediately package it on the fly into different formats—HLS, DASH, RTMP, and so on. At the same time, the DVR, which is their archive, operates as a separate module that continuously records the stream.&lt;/p&gt;

&lt;p&gt;So, roughly speaking, their architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  ┌──────────────► DVR / Archive
                  │
Camera ──► Ingest
                  │
                  └──────────────► JIT Packaging
                                      │
                                      ├── HLS
                                      ├── DASH
                                      └── other protocols
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, there is an important nuance when it comes to the term &lt;strong&gt;On Demand&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Flussonic, &lt;code&gt;ondemand&lt;/code&gt; primarily refers to the &lt;strong&gt;lifecycle of the input stream itself&lt;/strong&gt;: if there are no viewers, the source is shut down, and when a request appears, the stream is started again. This is more about saving resources on ingestion and traffic.&lt;/p&gt;

&lt;p&gt;Our implementation takes a somewhat different approach.&lt;/p&gt;

&lt;p&gt;The stream itself and recording &lt;strong&gt;never depend on whether there is a viewer&lt;/strong&gt;. The RTSP stream is always being ingested, the ring buffer remains active, and the recorder writes the archive 24/7.&lt;/p&gt;

&lt;p&gt;On-demand behavior applies only to the heaviest part of the pipeline—the &lt;strong&gt;HLS packaging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, our architecture looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No viewer
     ↓
Do not create the HLS muxer
     ↓
Ingest keeps running
     ↓
Archive keeps running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on that, it would be more accurate to say that Flussonic’s On Demand manages the lifecycle of the source itself—the RTSP connection—while Lazy Muxing in our implementation exclusively manages the lifecycle of the heavy pipeline component, the HLS packaging layer, while keeping the source RTSP stream hot and active for recording.&lt;/p&gt;

&lt;p&gt;At the same time, Flussonic’s overall approach is, of course, much broader than our implementation. It is a full-featured system for delivering a single source through multiple output protocols.&lt;/p&gt;

&lt;p&gt;Our task is much narrower: not keeping HLS multiplexing active for cameras that nobody is currently watching.&lt;/p&gt;

&lt;p&gt;This allows us to tightly tie the lifecycle of the muxer to the presence of an actual HLS viewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Late viewers and low-latency startup
&lt;/h2&gt;

&lt;p&gt;Another problem with On Demand is its relatively slow “wake-up” time.&lt;/p&gt;

&lt;p&gt;The client presses Play—or autoplay is enabled, but they have just opened the browser or player—and... they are staring at a spinning loader for 5–15 seconds.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;The issue lies in the HLS protocol itself. An HLS segment must begin with an I-frame, or keyframe.&lt;/p&gt;

&lt;p&gt;As a result, the muxer wakes up and simply waits for the camera to send the next keyframe. If the camera’s GOP is configured for 2, 3, 4, or 5 seconds, the client will have to wait that long, plus the buffering time of the player itself.&lt;/p&gt;

&lt;p&gt;In the worst cases, the total time from the first click to the appearance of an image can approach 30 seconds.&lt;/p&gt;

&lt;p&gt;Nobody likes latency. HTTP/3 is on the horizon, and one of its advantages is precisely reduced latency.&lt;/p&gt;

&lt;p&gt;So what did we do to work around this?&lt;/p&gt;

&lt;p&gt;When the lazy muxer starts, it does not wait for new frames at all. Instead, it performs a dump of the RingBuffer. Since the buffer is circular—and we keep roughly 30 frames in it—it is guaranteed to contain the previous keyframe.&lt;/p&gt;

&lt;p&gt;The muxer immediately extracts this historical GOP, assembles the first &lt;code&gt;.ts&lt;/code&gt; segment, and serves it to the player.&lt;/p&gt;

&lt;p&gt;The viewer gets an image almost instantly, aside from the player’s own buffering, which obviously does not disappear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/hls/muxer.go&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Muxer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Get a channel where the RingBuffer will IMMEDIATELY provide&lt;/span&gt;
    &lt;span class="c"&gt;// the historical GOP&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ringBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currentBuf&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// Ignore everything until the first historical I-frame from the dump&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsKeyFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c"&gt;// Instantly generate the first .ts segment!&lt;/span&gt;
            &lt;span class="n"&gt;tsWriter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mpegts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tracks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nuance here is that the video starts roughly a second behind real time, but for the viewer, the image appears almost immediately.&lt;/p&gt;

&lt;p&gt;There are limitations, of course.&lt;/p&gt;

&lt;p&gt;If your cameras—usually extremely cheap Chinese PTZ cameras—produce a keyframe once every 10 seconds, or God forbid even less frequently, then keeping such a large buffer in RAM becomes memory suicide.&lt;/p&gt;

&lt;p&gt;Our buffer is tuned for a relatively small number of frames and frequent keyframes.&lt;/p&gt;

&lt;p&gt;We are not trying to compensate for poor camera configuration by endlessly increasing the buffer size.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do we know that the viewer has left, or: a mini watchdog
&lt;/h2&gt;

&lt;p&gt;With WebRTC, everything is straightforward. The socket disconnects, the viewer is gone.&lt;/p&gt;

&lt;p&gt;With HLS, however, things are a little more complicated.&lt;/p&gt;

&lt;p&gt;It is plain HTTP. The player simply requests the &lt;code&gt;.m3u8&lt;/code&gt; file every few seconds.&lt;/p&gt;

&lt;p&gt;At first, there was an idea to implement sophisticated analytics based on TCP sessions, but in the end, it was decided not to overcomplicate things.&lt;/p&gt;

&lt;p&gt;Why reinvent the wheel when you can avoid it?&lt;/p&gt;

&lt;p&gt;Instead, we implemented a very simple, brute-force, and straightforward watchdog.&lt;/p&gt;

&lt;p&gt;I’ll just leave the code here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/stream/stream.go&lt;/span&gt;

&lt;span class="c"&gt;// 1. Called on every HTTP request to the .m3u8 playlist&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WakeUpHLSMuxer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Muxer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;muxerMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;muxerMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastHLSRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// Update the timer&lt;/span&gt;

    &lt;span class="c"&gt;// If the muxer is sleeping, start it&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hlsMuxer&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hlsMuxer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMuxer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ringBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hlsMuxer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// 2. Watchdog goroutine running in the background&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;lazyHLSWatchdog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;muxerMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c"&gt;// If there has been no activity for more than 60 seconds,&lt;/span&gt;
        &lt;span class="c"&gt;// gracefully shut down the muxer&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hlsMuxer&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastHLSRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hlsMuxer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hlsMuxer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// Release memory and CPU&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;muxerMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Words are great, but let’s look at the metrics.&lt;/p&gt;

&lt;p&gt;A lot of features look great on paper, and then in production you suddenly discover goroutine leaks.&lt;/p&gt;

&lt;p&gt;For testing, I wrote a utility that simulates 100 cameras running at 30 FPS.&lt;/p&gt;

&lt;p&gt;That is 3,000 frames per second.&lt;/p&gt;

&lt;p&gt;Two scenarios were compared:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;100 active HLS viewers + recorder&lt;/li&gt;
&lt;li&gt;100 sleeping HLS viewers, with only the recorder running&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The memory graph in the second scenario is a perfect sawtooth. The average baseline is 1.3 GB.&lt;/p&gt;

&lt;p&gt;And what happens to CPU usage?&lt;/p&gt;

&lt;p&gt;It drops by almost &lt;strong&gt;5×&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The core handles 3,000 FPS for continuous archive recording without even breaking a sweat, because it no longer has to perform multiplexing.&lt;/p&gt;

&lt;p&gt;Sleeping viewers genuinely free up server resources instead of simply sitting there as dead weight.&lt;/p&gt;

&lt;p&gt;In essence, you get stream density similar to a bare RTSP relay, while still retaining the convenience of web players and full archive recording.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;========================================
RUSEON Core Capacity Test
Cameras: 100 | Viewers: 100 (Sleeping) | Duration: 60s
========================================
[*] Starting cameras and pipelines...
[*] Load test running...
Memory: 1056 MB (Alloc) | GC Pauses: 10
Memory: 1660 MB (Alloc) | GC Pauses: 11
Memory: 1241 MB (Alloc) | GC Pauses: 12  &amp;lt;-- GC cleanup (Perfect sawtooth)
...
Memory: 2471 MB (Alloc) | GC Pauses: 14
Memory: 1355 MB (Alloc) | GC Pauses: 15
========================================
RESULTS
Frames Ingested: 181 800
Average Ingest FPS: 3026.68
Final Memory Alloc: 1517 MB (Baseline)
========================================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/RUSEGAL/ruseon-core" rel="noopener noreferrer"&gt;https://github.com/RUSEGAL/ruseon-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Writing terabytes to disk in Go: Stopping the OS Page Cache from eating all your RAM (FADV_DONTNEED)</title>
      <dc:creator>RUSEGAL</dc:creator>
      <pubDate>Sun, 09 Aug 2026 06:04:49 +0000</pubDate>
      <link>https://dev.to/rusegal/writing-terabytes-to-disk-in-go-stopping-the-os-page-cache-from-eating-all-your-ram-fadvdontneed-2cfa</link>
      <guid>https://dev.to/rusegal/writing-terabytes-to-disk-in-go-stopping-the-os-page-cache-from-eating-all-your-ram-fadvdontneed-2cfa</guid>
      <description>&lt;p&gt;Hello everyone! This is the second article about the development of RUSEON-core, a Zero-Copy video streaming server for AI platforms and Edge video infrastructure. In the &lt;a href="https://dev.to/rusegal/how-we-served-8-gbps-of-video-on-a-single-go-cpu-core-and-survived-the-thundering-herd-11"&gt;first article&lt;/a&gt;, I talked about the fundamental reason why we decided to create our own server in the first place. I also covered the main problem with most similar solutions — the "thundering herd" — and how we managed to squeeze out 8 Gbps on a single CPU core. By the way, I forgot to mention in that article that besides simple streaming, we also record the streams in fMP4 format. It’s stored locally for N amount of time, and it can fly off to an S3 bucket (depending on how long the clients want to keep the recordings).&lt;/p&gt;

&lt;p&gt;This article is precisely about a non-obvious (well, at least to me, maybe for someone else it's an everyday thing) problem related to data storage and its specifics across all Operating Systems. So, let's dive in.&lt;/p&gt;

&lt;p&gt;We rolled out our first release to production (100 cameras), made the clients happy, and started working. About an hour passed, and the alerts started flying. I SSH into the server, open htop, and see there's only 100 MB of free RAM. Uh-oh. I should clarify that the production server had 32 gigs of RAM. The expected behavior was that the CPU is chilling, the network card is chewing through the traffic, RAM usage is around 250-300 MB, and the disks are not heavily loaded. So, when you see numbers like that in htop, you start blaming yourself and your crooked hands that wrote this piece of "garbage". But still, we decided to go to Google, ChatGPT, and the like. Fortunately, the answer was found quickly, and we stopped beating ourselves up.&lt;/p&gt;

&lt;p&gt;The code was absolutely not the culprit; Linux itself ate the memory. If you've ever written tons of data to a disk, I think you already know what’s going on. There is an "invisible enemy" known as the Page Cache. That was exactly the root of this problem.&lt;/p&gt;

&lt;p&gt;How does the Page Cache work and what to do with it?&lt;br&gt;
When your function that is supposed to write data to the disk actually writes data, it doesn't write it to the disk. It writes it to RAM. The logic of the Linux kernel is simple and trivial, and it is aimed at accelerating the "responsiveness" of the system. The whole essence can be explained like this: "Oh, they just wrote a hundred gigabytes of data, they will probably need to read this data soon. Let me keep it in the cache, the user will be happy they could read it so fast." And so it goes, gigabyte after gigabyte, until the server runs out of physical memory.&lt;/p&gt;

&lt;p&gt;The typical solution to the problem is writing a bash script that runs echo 3 &amp;gt; /proc/sys/vm/drop_caches once an hour. And some people just ignore it and let the system kill random processes via the OOM Killer. But we are building a fault-tolerant thing. That doesn't work for us. The task is to explain to the OS kernel that our fMP4 video archive segments are write-only trash for a short amount of time (because if the client doesn't want to keep records for long, the archive gets cleaned up, and if they do, we send the archive to S3 after N time, and still clean up the local copy). So everything should work on the principle of "write and forget".&lt;/p&gt;

&lt;p&gt;How to tame the Linux kernel via Go (the right way)&lt;br&gt;
In C/C++, there is a system call for this: posix_fadvise. You can explicitly tell the OS exactly how you will be working with the file. In Go, this doesn't exist out of the box. But there is a very good package, golang.org/x/sys/unix, which allows you to easily replace the system call. The flag is called FADV_DONTNEED. We literally say to the kernel: "We wrote it, flush it to disk and get the hell out of the cache."&lt;/p&gt;

&lt;p&gt;But there is one cruel nuance here, which I stumbled upon myself and racked my brain over for a long time (and all it took was reading the docs, but here, just like with assembling IKEA furniture: "Why do I need a manual, I know how to do it myself"). The Linux kernel does not remove pages from the cache if they are so-called "dirty" — meaning they haven't been physically written to the disk platter yet. If you just call Fadvise, nothing will happen.&lt;/p&gt;

&lt;p&gt;First, you need to do a hard Sync().&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// File: pkg/storage/localfs/file_linux.go&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;localfs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"golang.org/x/sys/unix"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;FileWrapper&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FileWrapper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DropCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// First, flush dirty pages to disk!&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// And only then order the kernel to forget them&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fadvise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FADV_DONTNEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architecture: How not to break the build on Windows&lt;br&gt;
System calls are almost always a huge cross-platform headache. On Windows, the FADV_DONTNEED flag simply does not exist (hello there, Microsoft! Are you guys doing okay?). If you don't split the code for different OSs, the compiler will just tell you to get lost.&lt;/p&gt;

&lt;p&gt;Therefore, a rather elegant (I invite you to argue this statement in the comments) interface was implemented. In the core of the recorder, there is now a check to see if the file descriptor knows how to drop the cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// OPTIMIZATION: Saving RAM from Page Cache&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dropper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheDropper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dropper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DropCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then comes the magic of Go build tags (thank you for those). In the file_linux.go file (with the //go:build linux tag), we call unix.Fadvise. And right next to it lies the file_others.go file (with the //go:build !linux tag), where the DropCache() method just does a regular Sync() and returns. The code is crystal clear, the linters are happy, and the build works everywhere.&lt;/p&gt;

&lt;p&gt;So, what was the result?&lt;br&gt;
We roll out the fixes, launch the exact same 100 cameras. I open the dashboard. The memory consumption graph looks almost like a perfect straight line. The server grabbed its rightful 250 megs for the Go process heap — and that's it. Hooray, victory! There is no more massive Page Cache growth. No processes are being evicted to swap. The server honestly writes tens of gigabytes per hour, and RAM is at peace.&lt;/p&gt;

&lt;p&gt;By the way, when you are backing up databases, parsing giant logs, or simply writing heavy files — this feature will save you a mountain of headaches. I also don't understand why this flag is barely talked about or written about anywhere. Usually, people only write about how to properly allocate slices, but there is complete silence regarding the fact that at the level of file operations, your OS can reduce all your efforts to zero.&lt;/p&gt;

&lt;p&gt;In short, in the open-source part of ruseon-core, this logic is now wired deep into the recording engine. The conclusion and advice I want to give is — don't blindly trust the kernel with memory, hoping that the OS is hypothetically "perfect and maximally thought out." Sometimes you have to slap the kernel on the wrist. Otherwise, you will run into similar problems as I did.&lt;/p&gt;

&lt;p&gt;The source code, as always, is available on GitHub: &lt;a href="https://github.com/RUSEGAL/ruseon-core" rel="noopener noreferrer"&gt;https://github.com/RUSEGAL/ruseon-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How We Served 8 Gbps of Video on a Single Go CPU Core (And Survived the Thundering Herd)</title>
      <dc:creator>RUSEGAL</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:24:02 +0000</pubDate>
      <link>https://dev.to/rusegal/how-we-served-8-gbps-of-video-on-a-single-go-cpu-core-and-survived-the-thundering-herd-11</link>
      <guid>https://dev.to/rusegal/how-we-served-8-gbps-of-video-on-a-single-go-cpu-core-and-survived-the-thundering-herd-11</guid>
      <description>&lt;p&gt;&lt;strong&gt;Foreword&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is a small side project I've been working on – relaying raw RTSP streams into HLS. Essentially, the goal is simple: allow any ordinary user to view the feed from their cameras without using proprietary cloud software from the camera manufacturer. First, that costs a lot of money, as many clients want to store camera recordings for quite a long time. Second, almost all camera manufacturers have different software, and it's simply inconvenient to have 10 different apps and/or portals just to view it all. And third, very few offer AI integrations (which is critically important for my clients). Even if this integration exists, it is either highly specialized, proprietary again, or costs a lot of money—and sometimes all of these combined. The solution is to use raw RTSP streams; 90% of all cameras on the market support and provide them.&lt;/p&gt;

&lt;p&gt;So, the setup was quite simple: a few cameras, a simple backend, and we serve the video via HLS on a single resource for clients, using the hls.js player. Everyone was happy with it, everyone liked it. Fast, simple, convenient, and cheap. Tests worked perfectly, clients were satisfied. There was a chat with each client where the viewing link was shared, as well as a general chat with all clients where connection issues, financials, and other things were discussed. And then the moment of truth arrived: someone mistakenly dropped a link into the general chat... and that was the end. Apparently, the entire chat clicked on the link. 3 minutes in, alerts started flying in the bot: alarm, achtung, panic. We check the hardware: the server is down, OOM, all streams dropped. 20 minutes later, the chat was exploding with angry messages about nothing working for anyone. Curtain drop.&lt;/p&gt;

&lt;p&gt;Welcome to the Thundering Herd problem.&lt;/p&gt;

&lt;p&gt;If any of you have tried serving live video, you definitely know this problem. When hundreds or even thousands of viewers try to watch the same stream, a standard server's logic is simple – open a new connection or spawn a separate process for each viewer. Generally, the result of this action is always the same – OOM, and the processor is dead.&lt;br&gt;
The simplest way to solve this would have been renting a more powerful cloud instance or buying new hardware. There's a saying that "if a problem can be solved with money, it's not a problem," but there was no money. Plus, we are engineers; consider it a challenge thrown at your professionalism. The idea emerged to write our own engine in GO. It’s cheap, and you can squeeze out maximum performance. And honestly, your own thing in GO – it's cool, trendy, and hip. The spoiler won't be long: in the end, we squeezed out 8.8 Gbps on a single CPU core, while the garbage collector was basically taking a vacation during all this.&lt;/p&gt;

&lt;p&gt;Here’s how Ruseon Core was put together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not the great and mighty FFmpeg?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s be honest, FFmpeg is an "axiom" in the world of any video processing. It's awesome, truly. Essentially, it's the standard for ages. But my god, does it love to "eat." Launching even a hundred FFmpeg processes for restreaming is equivalent to suicide. We specifically want to get away from the same problems it has. Even if you use it as an ingest proxy, the overhead will zero out all the benefits.&lt;br&gt;
While searching for a solution, MediaMTX came up. It’s a gorgeous project and essentially solves our "main" problem. But (I think "but" is becoming my favorite word, hehe), we need seamless integration with the AI pipeline, as well as archive recording. Out of the box, it doesn't have this, and writing plugins or wrappers takes a long time. Plus, what's the point if we already decided to solve the problem conceptually? Besides, "showing off" to clients that we only use our own proprietary development is a sweet deal. It also adds weight in the eyes of other engineers and companies.&lt;br&gt;
So we wrote an engine from scratch, while trying to keep it "modular." That means the ability to embed it or use it as an SDK. The main rule during development – absolutely no transcoding (we just move bytes around), minimum overhead, maximum performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The main feature: Zero-Copy RingBuffer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a new frame arrives from the camera (usually it's H.264, though we implemented the 265 codec too), it’s just another chunk of bytes.&lt;br&gt;
Imagine this "brilliant" idea: we distribute this chunk to thousands of viewers by copying the bytes into separate response buffers. Pictured it, right? In the world of GO – that's a path to success (sarcasm). The garbage collector will be absolutely thrilled, trying somehow to deal with a mountain of trash. The CPU, instead of streaming, will be entirely busy cleaning up.&lt;br&gt;
We decided to go a different route. We made a Zero-Copy RingBuffer and attached a &lt;code&gt;sync.Pool&lt;/code&gt; to it.&lt;/p&gt;

&lt;p&gt;How it works in theory:&lt;br&gt;
A frame arrives from the camera.&lt;br&gt;
We take an empty byte buffer from a pre-allocated pool.&lt;br&gt;
We write the frame into it (just once!).&lt;br&gt;
We hand out a pointer to this buffer to a thousand viewers.&lt;br&gt;
When everyone has read it — we return the buffer back to the pool.&lt;/p&gt;

&lt;p&gt;No allocations. No garbage collection pauses. 250 MB of RAM, 1-2% CPU load at 100 streams.&lt;/p&gt;

&lt;p&gt;In benchmarks, it looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BenchmarkWriteFrame-12    13.9 ns/op      0 B/op       0 allocs/op&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here you can experience a true engineering "orgasm." When streaming video, seeing zero data volume per operation. When the processor is grinding out tens of thousands of frames, and the heap remains as minimal as it was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests and load testing, where would we be without them?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm telling you all about how this was implemented, but we all love numbers (especially the numbers in a bank account). Writing fast code is cool, but you need to understand how it works in reality and where its limit is. For testing, we chose k6 by Grafana; it allows us to emulate exactly the problem that started this whole project.&lt;br&gt;
Test scenario: 1000 users hammering our muxer, constantly downloading the playlist (index.m3u8), and snatching megabyte-sized .ts chunks as fast as the server allows.&lt;br&gt;
There were thoughts that the "bottleneck" would start at 300 users... but thankfully I was wrong.&lt;/p&gt;

&lt;p&gt;70 seconds of testing on a single core of a workstation Ryzen 5600x:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data_received..................: 81 GB  1.1 GB/s
http_req_failed................: 0.00%  ✓ 0 ✗ 60822
http_req_duration..............: avg=3.13ms p(95)=6.13ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 8.8 Gbps of throughput (Carl!). 60 thousand successful HTTP responses. Not a single dropped connection.&lt;br&gt;
Big numbers, looks nice, but what’s the catch? It’s simple here, the HLS segments are just sitting in RAM. When a massive request for a segment occurs, the server simply serves the exact same bytes from the cache. That’s it. The disk is resting, there's no repackaging, the processor is ordering a whiskey and cola.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nuances&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The question of infinite buffer accumulation arises, but there won't be infinite buffer accumulation (and subsequent OOM), as some protection against this is implemented:&lt;br&gt;
The base Go network stack (&lt;code&gt;net/http&lt;/code&gt;), which essentially operates at the delivery level. The server just hands a static chunk of memory to the socket. That's it. Whether it downloads fast or slow - it doesn't matter.&lt;/p&gt;

&lt;p&gt;But at the core level (&lt;code&gt;Ring.go&lt;/code&gt;), the protection implementation is more interesting - inside the core, subscribers (HLS Muxer or AI workers) read frames through channels in Go. Writing to the channel is implemented as a non-blocking send (via &lt;code&gt;select&lt;/code&gt; + &lt;code&gt;default&lt;/code&gt;). The channel has a strictly defined depth. If a subscriber lags, its channel gets clogged, the core doesn't wait for it and doesn't allocate new memory. Here the &lt;code&gt;default&lt;/code&gt; branch kicks in - the frame is dropped for that specific subscriber. Here, unfortunately, you have to "sacrifice" a bad client for the sake of, say, 1000 "good" ones.&lt;br&gt;
There's also the common problem of resynchronization and lag, which is solved as follows. If a subscriber inside the core drops a frame due to lagging, they cannot be given the next P-frame, because the picture will fall apart (by the way, this is even mentioned in &lt;code&gt;gortsplib&lt;/code&gt;, the RTSP library used in the project). In this case, the core sets the &lt;code&gt;NeedsIFrame&lt;/code&gt; flag to true for them. The subscriber stays silent until the next I-frame (keyframe) arrives; from there, reading resumes cleanly and with minimal losses. In real-world operation, this takes milliseconds and is essentially unnoticeable to the end viewer. Especially since the muxer keeps a "sliding window" of the last 5 segments in memory. If the end viewer has a really bad connection, when trying to download an outdated segment, they will get a 404. The player on the client (in 99% of cases, the player users have is &lt;code&gt;hls.js&lt;/code&gt; or its implementations) catches the 404, realizes it has fallen behind the live feed, and jumps to the current segment, synchronizing with the rest.&lt;/p&gt;

&lt;p&gt;And an important point: the HLS protocol is a PULL implementation. This means that an infinite accumulation of lag will not happen. That happens when the server tries to shove data into a socket, the socket gets blocked due to the client's poor network, packets pile up in the queue, and when the network "clears its throat," the client starts watching a video from 10 minutes ago. The mechanics here are different:&lt;br&gt;
The muxer keeps a sliding window (Live Playlist) in memory — for example, only the last 5 segments (let's say, 10 seconds of video). Older segments are deleted forever.&lt;br&gt;
A client with a terrible connection pulls segment 100 for more than 3 minutes.&lt;br&gt;
Downloaded it, the player asks for the next segment, #101.&lt;br&gt;
But the freshest on the server is 200. Segment 101 is physically no longer in memory.&lt;br&gt;
The server serves an honest 404.&lt;br&gt;
The player catches the 404, downloads a fresh &lt;code&gt;index.m3u8&lt;/code&gt;, sees that the current segment is already 200, and jumps to the Live edge.&lt;/p&gt;

&lt;p&gt;A client with a bad network will simply see constant "jumps" forward and buffering (which is logical with a dead network), but they will not force our server to store a personal 10-minute cache for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is this not a perfect mechanism?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this implementation, your CPU is essentially chilling, load is at 1-2%. But at the same time, the load on the network will be colossal. Based on tests, 9 Gbps is basically the limit of a 10 Gigabit interface. The code will hold up, but the network card will start dropping packets. Physics has its limits. Also, don't forget that when we write about 0 B/op and 250 MB of RAM consumption for 100 streams, this refers only to userspace memory (the heap), the one the garbage collector is responsible for. Socket buffers aren't going anywhere. Hundreds or thousands of connections will eat up their rightful megabytes of memory for &lt;code&gt;tcp_mem&lt;/code&gt;. This rather describes that the problem doesn't multiply at the software level, i.e., we are not allocating gigabytes of structures inside GO. You have to understand that OS memory is an unavoidable tax on networking.&lt;br&gt;
The trickiest part is not messing up the &lt;code&gt;sync.Pool&lt;/code&gt; implementation. Otherwise, your picture will just fall apart, and some frames will simply be green. All because another thread is already writing new bytes in there.&lt;br&gt;
In short, the point of this article is that if you don't need transcoding, or you need minimal hardware load – then you shouldn't use heavy artillery like FFmpeg and the like. Control your memory. Move bytes around without copying.&lt;br&gt;
The source code and load testing scripts are located in the Ruseon Core repository in the &lt;code&gt;benchmarks/&lt;/code&gt; folder. Go ahead and try to crash your computer (I've killed mine more than once).&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/RUSEGAL/ruseon-core" rel="noopener noreferrer"&gt;https://github.com/RUSEGAL/ruseon-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
