Skip to main content
Back to articles
2026 / 08
| 4 min read

Sharing one radio sample stream

What rtlmux shares between clients, what happens when one falls behind, and why TCP reads don't line up with tuner commands.

sdr networking c buffering
On this page

Several programs can make use of the same radio samples. One might be displaying a spectrum while another is decoding something within it, and there is no need for each to own a separate receiver if they can work from the same tuned bandwidth. They do need to agree on what the receiver is doing.

rtlmux takes one upstream rtl_tcp connection and relays its sample stream to several clients. The project dates back to 2016; the August 2026 work hardened the runtime and led to the 1.0.0 release. Looking at that version, the useful details are mostly around the edges of the relay: tuning commands, clients that read at different speeds, and connections disappearing halfway through something.

One tuner shared by several sample consumersrtl_tcp sends one stream to rtlmux, which sends the same samples through separate queues to three clients. This drawing shows the sample direction; all clients share the upstream tuning settings.

Samples

One rtl_tcp tuner

rtlmux

Queue A

Queue B

Queue C

Every client receives the same upstream samples. A supported frequency or sample-rate command changes that one upstream receiver, so another client cannot carry on receiving an unrelated frequency through the same relay. The last stored settings are replayed when the upstream connection returns. That is useful shared state, provided the clients are arranged with that sharing in mind.

A slow reader gets its own queue

A new sample block is allocated once, then referenced from the output buffers of the clients able to accept it. Reference counting keeps the block alive until those consumers finish. Each client has its own queue, which lets a brief delay in one connection be absorbed without waiting before sending to the others.

There is a limit to how much a queue can help. If a client continuously reads more slowly than samples arrive, its backlog will continue growing. This excerpt from the ready-client loop shows what rtlmux does when a queue is already over four mebibytes:

struct evbuffer *ev = bufferevent_get_output(client->bev);
if(evbuffer_get_length(ev) > 4*1024*1024) {
  client->data.dropped += data->len;
  client->data.droppedCount ++;
  continue;
}

The new block is skipped for that client, and the loop continues to the next one. The old queued bytes aren’t cleared. This also isn’t a strict four-mebibyte ceiling on the process: the comparison is >, another accepted block can cross the threshold, and there are buffers elsewhere in the connection.

What that buys is a boundary around one client’s backlog. It also means the client can lose sample blocks and its received signal has gaps. That is a consequence a decoder has to tolerate or recover from; the relay cannot make up the samples later while continuing to follow the live stream.

For a simple model, let the incoming rate be R and the client’s sustained reading rate be C. Starting with an empty queue, and assuming C is smaller than R, the time to accumulate four mebibytes is:

seconds = 4 × 1024 × 1024 / (R - C)
Assumed incoming rateClient readsTime to accumulate 4 MiB
4 MiB/s0 MiB/s1 s
4 MiB/s2 MiB/s2 s
4 MiB/s3 MiB/s4 s

Those are calculations with constant illustrative rates, not measured SDR throughput or a latency guarantee. They leave out chunk sizes and kernel buffers. They do show why adding a few more megabytes only delays the problem for a persistently slow reader.

Five-byte commands can arrive in several reads

The reverse direction is much smaller but has a similar need to account for boundaries. An rtl_tcp command is five bytes: a command byte and a four-byte parameter. TCP may deliver that in one read, split it across several reads, or put several commands into one read.

The command parser waits until it has at least five bytes, removes one complete command, then checks whether another complete command is available. A connection callback is an opportunity to read data, rather than evidence that a whole command arrived. The initial twelve-byte RTL header needs the same care before clients can be marked ready to receive samples.

The release’s loopback tests exercise both cases, along with shutdown during reconnect and a new first client arriving after delayed restart. Those tests passed again on 2 October 2026. They establish particular connection behaviours without needing a physical radio, which is useful because a truncated header or a split command can be reproduced much more directly in a test than by hoping the network happens to deliver it that way.

With -d -r, the relay connects upstream when the first client arrives and disconnects after the last one leaves. It keeps listening for another client, which can start a new upstream connection and receive its header and samples. That last transition is covered by the delayed-restart test: the first group of listeners can finish without requiring somebody to start the relay again for the next group.