Network Backends

HORUS automatically selects the optimal communication backend based on topology — no configuration needed. Every topic is shared-memory backed; cross-machine replication is available separately via the opt-in net feature.

ℹ️Network Transport Is Opt-In

HORUS ships transparent LAN replication over UDP in the horus_net crate, but shared memory is the only transport until you turn it on — see Network Transport (LAN replication) below.

Automatic Backend Selection

When you call Topic::new("name"), HORUS automatically detects the optimal backend based on the number of publishers, subscribers, and whether the message type is POD:

use horus::prelude::*;

// Just create a topic — backend is auto-selected
let topic = Topic::<CmdVel>::new("motors.cmd_vel")?;
topic.send(CmdVel::new(1.0, 0.0));

No configuration needed. The backend upgrades as participants join.

Shared-Memory Backends

Every topic is shared-memory backed, so every backend is cross-process. There are five SHM backends; Topic::new() picks one of the four auto-selected ones from the publisher/subscriber topology and whether the message type is POD:

BackendLatencyDetection Criteria
FanoutShm~40nssubs > 1, non-POD (broadcast)
PodShm~50nssubs > 1, POD (broadcast)
MpscShm~65nspubs > 1, subs <= 1
SpmcShm~70nsnever auto-selected — see below
SpscShm~85nspubs <= 1, subs <= 1

SpmcShm exists as a backend but detection never chooses it: its consumers share one tail and compete for messages, so a fast consumer would starve the others — the wrong semantics for pub/sub broadcast. Multi-consumer topics get PodShm or FanoutShm instead, where each subscriber reads the stream independently and a fast consumer cannot starve a slow one. Independent is not the same as lossless: these rings are latest-wins, so a subscriber that falls a full lap behind skips the messages the publisher overwrote.

The backend is selected based on:

  • Topology: Number of publishers and subscribers
  • Data type: POD (fixed-size, memcpy-able) types get the dedicated zero-copy path; variable-size types are serialized with bincode, which costs more per message than the POD path

Dynamic Migration

HORUS migrates to a wider backend as participants join:

Single publisher + single subscriber
  → SpscShm, ~85ns

Second subscriber joins (non-POD payload)
  → FanoutShm, ~40ns

A second publisher joins as well
  → FanoutShm, ~40ns (no migration — non-POD broadcast already uses the fanout matrix)

Migration is transparent — send() and recv() calls are unaffected.

The ladder only climbs. Backend detection reads the topic's registration counts, and nothing on the send or receive path decrements them: dropping a Topic releases its keep-alives but does not deregister. A participant's slot is retired only after its 5-second lease has expired and something sweeps for it — in practice a new registration on that topic — and the sweep retires a slot only if the owning process is dead. A slot left behind by an ended thread of the current process is reclaimed only by a later registration in that same process. Until one of those happens, a topic that reached FanoutShm stays on FanoutShm however many subscribers have gone.

Performance Characteristics

MetricShared Memory
Latency40-85ns backend transport; non-POD payloads add bincode serialization on top
ThroughputMillions msg/s
Zero-copyYes for POD types (via mmap); non-POD types are bincode-serialized
Cross-machineNot by shared memory itself — add the opt-in net feature / horus run --net for ~50μs LAN replication

Network Transport (LAN replication)

Cross-machine topics are replicated by the horus_net UDP replicator. It is opt-in — build the horus crate with --features net, or run horus run --net:

TransportLatencyUse Case
Shared Memory40-85nsSame-machine IPC
UDP LAN replication (net feature)~50μsMulti-robot, cross-machine topics

Topic::new() continues to auto-select the optimal SHM backend either way. With the net feature enabled, replication auto-starts on scheduler.run() and discovers peers by UDP multicast (default group 224.0.69.72, port 9100); topics you subscribe to but do not publish are imported by default (import = "auto"). Configure it with the [network] section of horus.toml or the HORUS_NET_* environment variables: opt out with .network(false), HORUS_NO_NETWORK=1, or [network] enabled = false; narrow the surface with [network] import / HORUS_NET_IMPORT and [network] deny_export.

The [network] section is translated into HORUS_NET_* variables on one path only: a horus run that launches several executables. A single-file run, a directory run, a horus.toml run, a workspace run and horus launch perform no translation, so on an ordinary single-binary robot [network] enabled = false does not stop replication and deny_export does not keep the topic it names on the machine. There, use .network(false) or the environment variables, which are read directly by the runtime in every case. See Configuration and Environment Variables.

Zenoh is not the transport. It is an optional bridge package (horus install horus-zenoh) for cases UDP multicast cannot reach — cross-subnet or WiFi links.

See Also

  • Topic - Shared memory architecture and Topic API
  • Core API - Topic creation and usage