Communication Patterns Overview

HORUS provides three communication primitives that cover all robotics messaging needs:

PatternPurposeExample
TopicStreaming data (pub/sub)Sensor readings, motor commands, video frames
ActionLong-running tasks (goal/feedback/result)Navigation, arm motion, calibration
ServiceRequest/response RPC (blocking call)Query state, trigger a one-shot command, config get/set

Topics and Actions work locally (shared memory) and across machines (network transport via configuration). A Service is built from a pair of Topics internally, so it follows the same transport rules.

Topic: Streaming Pub/Sub

Topic<T> is the primary communication primitive. It provides ultra-fast pub/sub with automatic optimization — a 16-byte message costs ~63ns to publish when both ends share a process and ~151ns one-way between processes on the reference machine.

use horus::prelude::*;

// Create a topic — backend is auto-detected
let pub_topic: Topic<f32> = Topic::new("sensor.temperature")?;
pub_topic.send(25.3);

// Multiple subscribers can listen (up to 16 participants per topic — see below)
let sub_topic: Topic<f32> = Topic::new("sensor.temperature")?;
if let Some(temp) = sub_topic.recv() {
    hlog!(info, "Temperature: {}", temp);
}

Automatic Optimization

You never choose a backend — HORUS detects the optimal one automatically based on:

  • Participants: How many publishers and subscribers are attached
  • Payload: Whether the message type is plain-old-data (zero-copy ring) or has to be serialized

Where the participants run is not a criterion. Every topic is shared-memory backed and every backend is cross-process, so the same path serves participants whether they share a thread, share a process, or live in separate processes — location changes the latency, not the choice:

Scenario (16 B message, p50)Latency
Same thread~20ns (publish)
Same process, separate threads~63ns (publish)
Cross-process, 1 publisher → 1 subscriber~151ns (one-way)
Cross-process, 1 publisher → many subscribers~191ns (one-way)
Cross-process, many publishers → 1 subscriber~279ns (one-way)

Measured by all_paths_latency on an Intel Core i7-10750H — see Benchmarks for the full table.

The communication path can change at runtime — if a second subscriber joins, HORUS automatically migrates to a multi-consumer path. A message in flight across that switch can be lost, so pre-warm a topic to its final topology before it carries traffic that matters; see Topics.

Key Characteristics

⚠️A topic holds at most 16 participants

Publishers and subscribers share one 16-slot table, counted per process and thread. The 17th registration fails, and that handle receives nothing for its whole life — the role stays unregistered, so every later recv() fails the same way and returns None.

It says so. The failure is logged at warn naming the topic, the count and the limit, rate-limited to once a minute per topic so a hot loop cannot flood the log. Sixteen is plenty for a normal robot, but a fan-out topic read by many nodes — or a process that opens the same topic from several threads — can reach it, and that warning is the first place to look when a subscriber that should be receiving never does.

  • Non-blocking send: send() returns () — it never returns an error. Broadcast topologies (multiple subscribers) overwrite the oldest slot — latest-wins, no backpressure. On a point-to-point ring send() retries briefly (spin + yield) and then drops the newest message if a live subscriber is draining the ring; if nothing is draining it — no subscriber yet, the subscriber's process gone, or tail frozen for a 5-second lease — it retires the oldest unread slot instead and accepts the new message, counting the retirement in dropped_count(). Use try_send() to get the message back on failure.
  • send_blocking is not a general answer to loss: on any broadcast-backed topic it returns Err(SendBlockingError::NoBackpressure) without sending. See Topics.
  • Non-blocking recv: recv() returns Option<T>. Returns None if no message available.
  • Zero introspection overhead: No timing, logging, or tracing in the default path. Content logging is off by default and is toggled by the topic's verbose header flag (horus_core::communication::set_topic_verbose), not by a builder method.

For full Topic API details, see Topic Communication.

Action: Long-Running Tasks

Actions handle tasks that take time to complete and benefit from progress feedback and cancellation. They follow the Goal / Feedback / Result pattern: a client sends one goal, the server streams feedback while it works, and the client gets a single result — or cancels partway through.

An action is three message types and a name. A server registers a handler for it; a client sends goals and receives a handle it can poll, await, or cancel.

This page is the overview. The full treatment — server configuration, preemption policies, goal states and priorities, server metrics, error handling and the horus action CLI — is in Actions.

You needRead
The Goal/Feedback/Result types and how to declare themDefining an Action
Handling goals, streaming feedback, honouring cancellationAction Server
Sending a goal and awaiting or cancelling itAction Client
What states a goal moves through, and what preempts whatGoal Lifecycle

Network Communication

By default, Topics and Actions use shared memory for ultra-fast communication between processes on the same machine. HORUS also supports zero-configuration peer discovery over UDP multicast (group 224.0.69.72, port 9100), letting robots on the same local network find each other automatically. Build with --features net — LAN replication is opt-in and off by default. Tune it with [network] in horus.toml or the HORUS_NET_* env vars.

When to Use What

ScenarioUseWhy
Sensor data streamingTopicContinuous data, latest value matters
Motor commandsTopicHigh frequency, low latency
Camera framesTopicStreaming, multiple consumers
Navigate to positionActionLong-running, needs progress feedback
Pick-and-placeActionMulti-step, cancellable
Calibration routineActionTakes time, reports progress
Emergency stopTopicMust be instantaneous, no handshake
Diagnostics broadcastTopicMany publishers, flexible topology
Query a parameter / one-shot commandServiceCaller needs an answer before continuing

Rule of thumb: If it's a continuous stream of data, use Topic. If it's a task you'd want to start, monitor, and potentially cancel, use Action. If the caller needs an answer back before it can continue, use Service.

Composing Patterns

A typical robot system combines topics and actions in a single node:

struct NavigationSystem {
    // Topics for streaming sensor data
    lidar_sub: Topic<LaserScan>,
    odom_sub: Topic<Odometry>,

    // Topics for publishing commands
    cmd_vel_pub: Topic<Twist>,
    status_pub: Topic<DiagnosticStatus>,

    // Action server for goal-based navigation
    nav_server: ActionServerNode<NavigateToGoal>,
}

The navigation action server subscribes to sensor topics internally, computes a path, publishes velocity commands via topics, and reports progress back to the action client — all using the same underlying shared memory infrastructure.

Next Steps