Workshop 3 Preview: Zenoh Fundamentals (Exercises 1-3)
Part 1 of 3: Core Pub/Sub, QoS, and Shared Memory Transport
What This Series Covers
ROSCon India 2025 Workshop 3 features 8 hands-on exercises exploring Zenoh middleware for ROS 2. This 3-part preview series helps you understand what youβll learn:
| Part | Exercises | Focus |
|---|---|---|
| Part 1 (This Post) | 1-3 | Fundamentals: Pub/Sub, QoS, Shared Memory |
| Part 2 | 4-5 | Remote: Cloud Router, mTLS Security |
| Part 3 | 6-8 | Advanced: Wireless, Congestion, NAT |
Visual Summary: 7 Eureka Moments
Exercise 1: Core Pub/Sub & Discovery
What Youβll Learn
The foundation of any middleware is how nodes find each other and exchange messages. Exercise 1 explores how Zenoh handles this differently from DDS.
DDS vs Zenoh Discovery
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DDS DISCOVERY β
β β
β Node A Node B β
β βββββββ βββββββ β
β β β βββββ Multicast βββββββββΊ β β β
β β DDS β (239.255.0.1) β DDS β β
β β β βββββ SPDP/SEDP βββββββββΊ β β β
β βββββββ (every 30s) βββββββ β
β β
β Problem: Multicast often blocked on WiFi, NAT, cloud β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ZENOH DISCOVERY β
β β
β Node A Router Node B β
β βββββββ ββββββββ βββββββ β
β β β ββTCPβββΊβzenohdββββTCPβββ β β β
β βZenohβ β β βZenohβ β
β β βββββββββββ βββββββββββΊβ β β
β βββββββ ββββββββ βββββββ β
β β
β Solution: Router-based, works over any TCP connection β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π― The Eureka Moment: βWhy Isnβt My Listener Receiving?β
If youβre coming from DDS and try this:
# Terminal 1
ros2 run demo_nodes_cpp talker
# Terminal 2
ros2 run demo_nodes_cpp listenerYouβll see this warning and nothing happens:
zenoh::net::runtime::orchestrator: Scouting delay elapsed before start conditions are met.
[WARN] [rmw_zenoh_cpp]: Unable to connect to a Zenoh router.
Have you started a router with `ros2 run rmw_zenoh_cpp rmw_zenohd`?
DDS nodes find each other automatically via multicast. Zenoh nodes need a router to broker discovery. This isnβt a limitation - itβs a feature that enables Zenoh to work across NAT, WiFi, and cloud where multicast fails!
The Correct Pattern: Router First!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ZENOH STARTUP SEQUENCE β
β β
β Step 1: Start the Router (Terminal 1) β
β ββββββββββββββββ β
β β rmw_zenohd β β Must start FIRST β
β β listening on β β
β β tcp://:7447 β β
β ββββββββ¬ββββββββ β
β β β
β Step 2: Nodes connect to router β
β ββββββββββ΄βββββββββ β
β β β β
β βΌ βΌ β
β βββββββββ ββββββββββ β
β βtalker β βlistenerβ β
β βTerm 2 β βTerm 3 β β
β βββββββββ ββββββββββ β
β β
β Now they discover each other via the router! β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Hands-On Commands
# Terminal 1: Start the Zenoh router FIRST
ros2 run rmw_zenoh_cpp rmw_zenohd
# Terminal 2: Start publisher (connects to router)
ros2 run demo_nodes_cpp talker
# Terminal 3: Start subscriber (connects to router, discovers talker)
ros2 run demo_nodes_cpp listener
# Now you'll see: "I heard: [Hello World: 1]"# Scout for running routers
zenoh scout
# Or check if rmw_zenohd is running
pgrep -a zenohWhy This Design is Actually Better
| Scenario | DDS (Multicast) | Zenoh (Router) |
|---|---|---|
| Same LAN | β Works | β Works |
| WiFi (multicast often blocked) | β Fails | β Works |
| Across subnets | β Fails | β Works |
| Through NAT/firewall | β Fails | β Works |
| Cloud deployment | β Fails | β Works |
The router pattern is what enables Exercises 4-8 (remote connectivity, cloud routers, NAT traversal).
Zenoh Configuration Files
This section covers official Zenoh configuration file locations presented at Workshop 3.
Zenohβs behavior is controlled by configuration files. The default configurations are installed with rmw_zenoh_cpp:
# Default configuration file location
/opt/ros/$ROS_DISTRO/share/rmw_zenoh_cpp/config/
# Two key files:
DEFAULT_RMW_ZENOH_ROUTER_CONFIG.json5 # For the Zenoh router (rmw_zenohd)
DEFAULT_RMW_ZENOH_SESSION_CONFIG.json5 # For all ROS 2 nodesRouter Configuration (DEFAULT_RMW_ZENOH_ROUTER_CONFIG.json5):
{
mode: "router",
listen: {
endpoints: ["tcp/[::]:7447"] // Listen on all interfaces, port 7447
},
scouting: {
multicast: {
enabled: true,
address: "224.0.0.224:7446"
}
}
}
Session Configuration (DEFAULT_RMW_ZENOH_SESSION_CONFIG.json5):
{
mode: "client",
connect: {
endpoints: ["tcp/localhost:7447"] // Connect to local router
},
scouting: {
multicast: { enabled: true },
gossip: { enabled: true }
}
}
You can specify custom configurations using environment variables:
# For the router
export ZENOH_ROUTER_CONFIG_URI=/path/to/custom_router_config.json5
ros2 run rmw_zenoh_cpp rmw_zenohd
# For ROS 2 nodes
export ZENOH_SESSION_CONFIG_URI=/path/to/custom_session_config.json5
ros2 run demo_nodes_cpp talkerConnecting to a Remote Robot
This section covers how to connect your workstation to a robot running Zenoh.
When your robot is running rmw_zenohd on a different machine, you need to configure your nodes to connect to it. The simplest method uses ZENOH_CONFIG_OVERRIDE:
# Configure your node to connect to robot's Zenoh router
export ZENOH_CONFIG_OVERRIDE='connect/endpoints=["tcp/192.168.1.2:7447"]'
# Start the router on your workstation (connects to robot)
ros2 run rmw_zenoh_cpp rmw_zenohd
# Your ROS 2 nodes now communicate with the robot!
ros2 topic list # Shows robot's topicsβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONNECT TO ROBOT β
β β
β Your Workstation Robot (192.168.1.2) β
β ββββββββββββββββ ββββββββββββββββββ β
β β
β ββββββββββββββββ ββββββββββββββββ β
β β rmw_zenohd ββββββ TCP connection βββββΊβ rmw_zenohd β β
β β (client) β :7447 β (router) β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β β β β
β ββββββββ΄ββββββββ ββββββββ΄ββββββββ β
β β rviz2 β β camera_node β β
β β teleop βββββββββ topics ββββββββββΊβ lidar_node β β
β β rqt β β odom_node β β
β ββββββββββββββββ ββββββββββββββββ β
β β
β ZENOH_CONFIG_OVERRIDE='connect/endpoints=["tcp/192.168.1.2:7447"]' β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Connect Just 1 Node (No Local Router Needed!):
You can also connect a single node directly to the robot without running a local router:
# No need for local rmw_zenohd - node connects directly!
export ZENOH_CONFIG_OVERRIDE='connect/endpoints=["tcp/192.168.1.2:7447"]'
ros2 run rviz2 rviz2This is useful when you just want to visualize or teleop without setting up a full local infrastructure.
Router Discovery via UDP Multicast
This section explains how Zenoh routers automatically discover each other.
On a local network, Zenoh routers can automatically discover each other using UDP multicast - no manual configuration needed!
How It Works:
- Gossip Protocol: Routers use a gossip protocol for node interconnections
- UDP Multicast Scouting: Routers broadcast their presence on
224.0.0.224:7446 - Auto-Connect: When routers discover each other, they automatically form a mesh
The Gossip Protocol (also called βepidemic protocolβ) is inspired by how rumors spread in social networks. Itβs a decentralized way for nodes to share information without a central coordinator.
How Gossip Works in Zenoh (ASCII Diagram):
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β GOSSIP PROTOCOL IN ACTION β
β β
β Step 1: Router A connects to Router B β
β βββββββ βββββββ β
β β A ββββββββββΊβ B β A tells B: "I know about router C" β
β βββββββ βββββββ β
β β
β Step 2: Router B now knows about C (even without direct connection) β
β βββββββ βββββββ βββββββ β
β β A ββββββββββΊβ B β Β· Β· Β· Β· β C β B can now connect to C! β
β βββββββ βββββββ βββββββ β
β β
β Step 3: Information spreads exponentially β
β βββββββ βββββββ βββββββ β
β β A ββββββββββΊβ B ββββββββββΊβ C β Full mesh formed! β
β ββββ¬βββ βββββββ ββββ¬βββ β
β β β β
β βββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Properties:
| Property | Description |
|---|---|
| Decentralized | No central server needed - each node shares what it knows |
| Scalable | Information spreads in O(log N) rounds for N nodes |
| Fault-tolerant | Works even if some nodes fail - no single point of failure |
| Eventually consistent | All nodes eventually learn about all other nodes |
Why Zenoh Uses Gossip:
- Works without multicast - Gossip works over unicast TCP connections
- NAT-friendly - Only needs outbound connections
- Self-healing - Network topology updates automatically as routers join/leave
- Efficient - Each router only needs to know a few peers, gossip spreads the rest
Gossip vs Multicast:
| Aspect | UDP Multicast | Gossip Protocol |
|---|---|---|
| Network | Same subnet only | Works across subnets |
| NAT | Blocked by NAT | Works through NAT |
| Speed | Instant discovery | Gradual (seconds) |
| Reliability | Can lose packets | Reliable (retries) |
| WiFi | Often blocked | Always works |
Configuration:
scouting: {
multicast: { enabled: true }, // Fast local discovery
gossip: { enabled: true } // Cross-network discovery
}
Best Practice: Enable BOTH multicast (for fast local discovery) and gossip (for reliability and cross-network discovery).
Enable Router Discovery via Config File:
// zenoh_router_discovery.json5
{
mode: "router",
scouting: {
multicast: {
enabled: true,
address: "224.0.0.224:7446",
autoconnect: { router: "true" } // Auto-connect to discovered routers
}
}
}
# Use the config
export ZENOH_ROUTER_CONFIG_URI=/path/to/zenoh_router_discovery.json5
ros2 run rmw_zenoh_cpp rmw_zenohdEnable Router Discovery via Environment Variable:
# Quick inline override without a config file
export ZENOH_CONFIG_OVERRIDE='scouting/multicast/enabled=true;scouting/multicast/autoconnect=["router"]'
ros2 run rmw_zenoh_cpp rmw_zenohdUDP multicast often doesnβt work on: - WiFi networks (many access points block multicast) - Cloud/VPN (different subnets) - Docker (without --net=host)
In these cases, use explicit connect/endpoints configuration instead.
Key Takeaways
- DDS uses multicast for peer-to-peer discovery (fast on LAN, problematic elsewhere)
- Zenoh uses TCP connections to routers (works everywhere, including WiFi and cloud)
- The
rt/prefix maps ROS 2 topics to Zenoh key expressions
Exercise 2: Quality of Service (QoS)
What Youβll Learn
QoS profiles determine how messages are delivered. Exercise 2 explores matching QoS between publishers and subscribers.
QoS Parameters Explained
| Parameter | Options | Use Case |
|---|---|---|
| Reliability | RELIABLE / BEST_EFFORT |
Control commands need RELIABLE; sensor streams can use BEST_EFFORT |
| Durability | VOLATILE / TRANSIENT_LOCAL |
TRANSIENT_LOCAL delivers last message to late subscribers |
| History | KEEP_LAST(n) / KEEP_ALL |
How many messages to buffer |
| Deadline | Duration | Alert if messages donβt arrive in time |
| Liveliness | AUTOMATIC / MANUAL |
Node health monitoring |
QoS Compatibility Matrix (DDS Only!)
Publisher Reliability Subscriber Reliability Compatible? (DDS)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RELIABLE RELIABLE β
Yes
RELIABLE BEST_EFFORT β
Yes
BEST_EFFORT RELIABLE β No (incompatible!)
BEST_EFFORT BEST_EFFORT β
Yes
With DDS middleware, a RELIABLE subscriber cannot receive from a BEST_EFFORT publisher. ROS 2 silently drops the connection - no error message!
Use ros2 topic info -v /topic_name to check QoS compatibility.
π― Eureka Moment #2: Zenoh Doesnβt Enforce QoS Incompatibility!
Hereβs what happens when you actually test this with rmw_zenoh_cpp:
# Terminal 2: Best Effort Publisher
ros2 topic pub /qos_test std_msgs/msg/String "{data: 'best effort msg'}" \
--qos-reliability best_effort --rate 1
# Terminal 3: Reliable Subscriber (should fail with DDS...)
ros2 topic echo /qos_test --qos-reliability reliable
# Result: Messages ARE received! π€―Wait, the matrix said this should fail!
The QoS compatibility matrix above applies to DDS-based middleware only (CycloneDDS, FastDDS).
Zenoh (rmw_zenoh_cpp) is not a DDS implementation - itβs a completely different protocol that doesnβt enforce DDS-style QoS incompatibility.
From ros2/rosbag2 PR #1936: > βrmw_zenoh_cpp is not a DDS-backboned RMW. The assumption of QoS incompatibility of reliability doesnβt mean the subscriber receives no message.β
DDS vs Zenoh QoS Behavior
| Scenario | DDS (CycloneDDS/FastDDS) | Zenoh (rmw_zenoh_cpp) |
|---|---|---|
| Reliable Pub β Reliable Sub | β Works | β Works |
| Reliable Pub β Best Effort Sub | β Works | β Works |
| Best Effort Pub β Reliable Sub | β Silent failure | β Works! |
| Best Effort Pub β Best Effort Sub | β Works | β Works |
Why This Matters
When you switch from DDS to Zenoh:
- Old debugging assumptions may not apply - βQoS mismatchβ isnβt why your subscriber isnβt receiving
- Zenoh is more permissive - it delivers what it can rather than refusing to connect
- Test your assumptions - behavior you learned with DDS may not transfer to Zenoh
Hands-On Commands
# Check QoS of a topic
ros2 topic info -v /chatter
# Test with DDS (switch middleware temporarily)
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 daemon stop && ros2 daemon start
# Now test the incompatible QoS - you'll see the failure
# Switch back to Zenoh
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 daemon stop && ros2 daemon start
# Same test - now it works!Zenoh QoS Mapping
While Zenoh doesnβt enforce compatibility, it still respects QoS intentions:
| ROS 2 QoS | Zenoh Behavior |
|---|---|
RELIABLE |
Zenoh reliable channel (retries on failure) |
BEST_EFFORT |
Zenoh best-effort channel (no retries) |
TRANSIENT_LOCAL |
Zenoh queryable + cache (late joiners get last value) |
Whatβs Next
In Part 2, weβll preview Exercises 4-5: - Exercise 4: Remote Connectivity & Cloud Router - connecting robots across networks - Exercise 5: mTLS Security - encrypting robot communications
These exercises show how Zenoh enables secure, global robot connectivity - something traditionally difficult with DDS.
Key Takeaways: 7 Eureka Moments
| # | Discovery | What We Learned |
|---|---|---|
| 1 | Router Required | Zenoh needs rmw_zenohd running first (unlike DDS multicast) |
| 2 | QoS Compatibility | Zenoh ignores DDS-style QoS incompatibility |
| 3 | Docker SHM | Need --shm-size=1g flag for SHM to work in containers |
| 4 | Resolution vs FPS | 720pβ10 FPS, 1080pβ5 FPS with synthetic data (CPU-bound!) |
| 5 | Know Your Bottleneck | SHM didnβt help synthetic data because cam2image is CPU-bound |
| 6 | Real Camera 720p | D435i achieves 30 FPS / 83 MB/s - no frame drops |
| 7 | Zenoh TCP is FAST | D435i 1080p: 188 MB/s @ 30 FPS without SHM! Transport isnβt the bottleneck |
Preparation Checklist
Before Workshop 3, make sure you can:









