Zenoh ROS 2 Bridge Tutorial: Hands-On RMW Setup (2026)
If you have ever watched a perfectly good ROS 2 stack fall apart the moment you moved a robot from a wired bench onto plant Wi-Fi, this zenoh ros2 bridge tutorial is written for you. The default DDS middleware that ships with ROS 2 was engineered for low-latency wired LANs with reliable multicast, and it shows: discovery storms, dropped participants, and ballooning bandwidth are the rule the instant packets have to cross a lossy radio link or a NAT boundary. Eclipse Zenoh takes a different stance. It treats the network as hostile by default, leans on a peer-plus-router topology that survives flaky links, and gives you explicit control over which data leaves a site. Over the next 4,500 words you will wire ROS 2 to Zenoh two ways — natively through rmw_zenoh and through the protocol-translating zenoh-bridge-ros2dds — then route a multi-robot fleet and stand up lossless teleoperation across a wide-area network.
What this covers: native RMW install and configuration, the DDS bridge for legacy robots, multi-robot namespacing, router-to-router WAN topology, and the gotchas that bite in production.
Context and Background
ROS 2 abstracts its transport behind the ROS Middleware (RMW) interface, and for most of the framework’s life the default implementation has been DDS — Fast DDS from eProsima, with Cyclone DDS as the common alternative. DDS is a capable standard, but its discovery model assumes an environment that simple Wi-Fi networks rarely provide. The Simple Participant Discovery Protocol floods multicast announcements so every participant learns about every other participant; on a busy LAN with dozens of nodes this generates a discovery storm whose traffic grows with the square of the participant count. Many enterprise and industrial access points disable or rate-limit multicast outright, at which point default discovery simply fails. Push the same stack across a WAN and the problems compound: DDS was never designed to traverse NAT, and tunnelling it reliably is painful.
Eclipse Zenoh — a project under the Eclipse Foundation, built by some of the same people who shaped DDS — was designed from the opposite direction. It is a unified protocol for data in motion, data at rest, and computations, with a routing layer that scales from microcontrollers to the cloud. Crucially for robotics, Zenoh offers a peer-to-peer mode for nodes on the same LAN and a router (or “infrastructure”) mode that brokers traffic across links where multicast and direct peering are impossible. It does unicast discovery through routers, supports TCP, QUIC, and other transports, and lets you scope exactly which key expressions cross a boundary. That last property — explicit, per-keyexpr routing — is what makes Zenoh so much easier to reason about over a WAN than raw DDS.
The cost difference is not subtle, and it is worth being precise about why. Under DDS’s Simple Discovery Protocol, every participant must exchange both participant-level (SPDP) and endpoint-level (SEDP) metadata with every other participant before any user data flows. For N participants that is on the order of N-squared metadata exchanges, and each new node entering the network triggers a fresh round across the whole mesh. On a thirty-node robot this is invisible; on a fleet of thirty robots sharing one domain it is a measurable, recurring tax that spikes every time a node restarts. Zenoh collapses that: a node tells its router what it produces and consumes, the router holds the routing table, and the matching happens centrally without every endpoint gossiping with every other endpoint. The discovery cost grows roughly linearly with participant count rather than quadratically, and — just as importantly — it is bounded by the router rather than amplified across an unreliable radio link. This is also why Zenoh degrades gracefully when a Wi-Fi link drops a burst of packets: there is one session to re-establish, not a full mesh to rediscover.
There are two ways to bring Zenoh into a ROS 2 system, and choosing correctly is half the battle. The first is the native path: rmw_zenoh, a full RMW implementation that replaces DDS entirely so your nodes speak Zenoh end to end. The second is the bridge path: zenoh-bridge-ros2dds, a standalone process that speaks DDS on one side and Zenoh on the other, letting an unmodified DDS-based robot join a Zenoh fabric without recompiling anything. The native path is cleaner when you control the whole fleet; the bridge is indispensable when you must federate robots you cannot rebuild. We will set up both. For background on the fleet-scale networking patterns this enables, see our companion piece on multi-robot fleet networking, and the upstream design rationale in the Eclipse Zenoh documentation.
System Overview and Prerequisites
The native RMW replaces DDS in your ROS 2 nodes, the optional bridge translates for legacy DDS robots, and a Zenoh router stitches sites together over the WAN. Set RMW_IMPLEMENTATION=rmw_zenoh_cpp, start the router with ros2 run rmw_zenoh_cpp rmw_zenohd, and your nodes discover each other through it instead of through DDS multicast.

Figure 1: Native rmw_zenoh nodes on Site A reach a legacy DDS robot on Site B through the zenoh-bridge-ros2dds, with two Zenoh routers peering over the WAN.
Figure 1 shows the topology you will build by the end of this tutorial. On Site A, ordinary ROS 2 nodes run with the native RMW and register with a local Zenoh router. On Site B, a legacy robot still running Fast DDS connects through zenoh-bridge-ros2dds, which converts its DDS traffic into Zenoh and hands it to a second router. The two routers peer over a single TCP connection across the WAN. Every topic, service, and action that needs to cross sites flows through that one link, and nothing else leaks. This is the core mental model: routers are the backbone, nodes and bridges are the edge, and key expressions decide what travels.
Picking your ROS 2 distribution
rmw_zenoh is supported on the modern ROS 2 distributions and reached its first round of official tier support around the Jazzy Jalisco release; it is not the default RMW — DDS still holds that role — but it is a first-class, officially maintained alternative you can opt into. Throughout this tutorial I assume ROS 2 Jazzy on Ubuntu 24.04, with notes where Humble (Ubuntu 22.04) differs. Confirm your environment before installing anything:
# Verify your ROS 2 distro and that the workspace is sourced
echo "ROS_DISTRO=$ROS_DISTRO"
source /opt/ros/jazzy/setup.bash
ros2 doctor --report | grep -i middleware
If ROS_DISTRO prints jazzy (or humble) and ros2 doctor reports a DDS middleware, you have a clean baseline to switch away from. Keep a second terminal open throughout — Zenoh work almost always involves at least a router process and a node process running side by side, and you will want to watch both.
Installing rmw_zenoh from binaries
On Jazzy the simplest path is the binary package from the ROS 2 apt repository. This pulls in the RMW library, the bundled Zenoh router daemon, and the small set of dependencies they need:
# Binary install on ROS 2 Jazzy (Ubuntu 24.04)
sudo apt update
sudo apt install ros-jazzy-rmw-zenoh-cpp
The package name follows the usual ros-<distro>-<package> convention with underscores converted to hyphens, so on Humble you would install ros-humble-rmw-zenoh-cpp instead. Binary packages are the right choice for production hosts and CI images because they are versioned, signed, and reproducible. The one caveat is lag: binaries trail the GitHub main branch by weeks, so if you need a very recent fix you may have to build from source.
Building rmw_zenoh from source
When you need the latest behaviour — or you are hacking on Zenoh itself — build into a dedicated overlay workspace so you never contaminate your system ROS install:
# Source build into an overlay workspace
mkdir -p ~/ws_zenoh/src && cd ~/ws_zenoh
git clone https://github.com/ros2/rmw_zenoh.git src/rmw_zenoh
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
A few notes on the steps above. rosdep install resolves the system and ROS dependencies declared in each package’s package.xml, so it must run after you have sourced the base ROS environment — otherwise it cannot see the distro. Building in Release mode matters more than usual here: Zenoh’s transport and serialization paths are CPU-sensitive, and a debug build can add real latency under load. Finally, sourcing install/setup.bash layers your overlay on top of /opt/ros/jazzy, so the freshly built rmw_zenoh_cpp takes precedence over any binary copy. Order matters — always source the base distro first, then the overlay.
Selecting and launching the middleware
With the library installed, two environment variables flip ROS 2 onto Zenoh. The first names the implementation; the second tells the RMW where to find its router configuration if you are not using the default:
# Select Zenoh as the active RMW
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
# Confirm the switch took effect
ros2 doctor --report | grep -i middleware
Setting RMW_IMPLEMENTATION per shell is fine for experimentation, but for anything durable export it from your launch environment, a systemd unit, or a container entrypoint so every process in the fleet agrees. A mismatch — one node on DDS, another on Zenoh — is one of the most common reasons “my topics vanished,” because the two stacks share no wire protocol and simply cannot see each other.
Now start the router. Unlike DDS, where discovery is fully decentralized, rmw_zenoh expects a router process to act as the discovery and routing point for nodes that cannot peer directly:
# Start the Zenoh router daemon (leave this running)
ros2 run rmw_zenoh_cpp rmw_zenohd
Leave that terminal running. In a second terminal — with RMW_IMPLEMENTATION=rmw_zenoh_cpp exported — launch the classic talker and listener to prove the path end to end:
# Terminal 2: publisher
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run demo_nodes_cpp talker
# Terminal 3: subscriber
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run demo_nodes_cpp listener
If the listener prints the talker’s “Hello World” counter, your nodes are communicating over Zenoh rather than DDS. You can confirm there is no DDS traffic by stopping the router: with the daemon down, single-host nodes can still discover via Zenoh’s gossip on the local machine in some configurations, but cross-host discovery stops cold — which is exactly the controlled behaviour we want. The router is the seam you will later stretch across the WAN.
It is worth understanding what just happened underneath the demo, because it generalizes to every node you will ever run. When the talker started, rmw_zenoh opened a session to the router and declared a publisher keyed on an expression derived from the topic name and type. When the listener started, it declared a subscriber on the matching key expression. The router compared the two declarations, found they matched, and installed a forwarding rule. From that point on, each message the talker publishes is serialized once, handed to the router as a Zenoh sample, and forwarded to the listener — and to any other subscriber that later declares interest in the same key. Nothing is broadcast speculatively. If you kill the listener, the router tears down the forwarding rule and the talker’s samples go nowhere, consuming no link bandwidth. That publish-without-cost-when-unsubscribed behaviour is the single most important property to internalize, because it is what makes per-prefix WAN forwarding both cheap and predictable later in this tutorial. The same declaration-and-match dance happens for services and actions, which Zenoh models as queryable resources rather than as separate discovery channels.
What you need before the walk-through
Before moving on, make sure you have: a working ROS 2 Jazzy or Humble install on each machine; rmw_zenoh_cpp installed (binary or source) on every native node host; network reachability on TCP port 7447 between any machine running a node and the router it connects to; and, if you are federating a legacy robot, a host that can run zenoh-bridge-ros2dds with line-of-sight to that robot’s DDS domain. With those in place, the rest is configuration.
Walk-through: Bridging, Multi-Robot Routing, and WAN
Discovery in Zenoh is router-mediated rather than multicast-flooded, which is what lets it cross links DDS cannot. Figure 2 traces the exact handshake.

Figure 2: Two nodes connect to a Zenoh router on TCP 7447, declare their key expressions, the router matches publisher to subscriber, and samples then flow through the router.
Both nodes open a TCP session to the router on port 7447. Each declares the key expressions it cares about — a subscriber declares interest, a publisher declares it will produce. The router maintains the routing table that matches them and forwards samples only along paths where a matching subscriber exists. There is no flooding: a publisher with no subscribers sends nothing across the link. This is the mechanism that keeps WAN bandwidth proportional to useful traffic rather than to participant count.
Writing a router configuration
The default router works for a single LAN, but the moment you want to connect sites you need an explicit config. Zenoh routers read a JSON5 file; the two fields that matter most are listen (endpoints this router accepts connections on) and connect (endpoints this router dials out to). Here is a Site A edge router that listens locally and connects to a cloud relay:
{
mode: "router",
listen: {
endpoints: ["tcp/0.0.0.0:7447"]
},
connect: {
endpoints: ["tcp/relay.example.com:7447"]
},
scouting: {
multicast: { enabled: false }
}
}
Read that field by field. mode: "router" makes this an infrastructure node that brokers for others rather than a peer. listen.endpoints binds TCP on all interfaces at port 7447 so local nodes and bridges can reach it; in production you would bind a specific interface, not 0.0.0.0. connect.endpoints tells this router to actively dial the relay — outbound connections traverse NAT cleanly, which is why edge sites connect out to a publicly reachable relay rather than waiting to be dialled. Finally, disabling multicast scouting is deliberate: on a WAN, or any network where multicast is unreliable, you want discovery to happen only through the explicit endpoints you configured. Launch it with ros2 run rmw_zenoh_cpp rmw_zenohd pointing at the file via the ZENOH_ROUTER_CONFIG_URI environment variable.
Bridging a legacy DDS robot
A robot you cannot recompile still speaks DDS. zenoh-bridge-ros2dds joins its DDS domain, mirrors every ROS 2 topic, service, and action into Zenoh key expressions, and forwards them to a router:
# Bridge ROS 2 DDS domain 0 into the Zenoh fabric
zenoh-bridge-ros2dds \
--connect tcp/relay.example.com:7447 \
-d 0
The -d 0 flag sets the DDS domain ID the bridge listens on — it must match the ROS_DOMAIN_ID of the robot you are federating. --connect points the bridge at the same relay your native routers use, so the bridged robot lands on the identical fabric as everything else. By default the bridge maps a ROS 2 topic such as /scan to a Zenoh key expression under a namespace it derives from the topic name, preserving the hierarchy so downstream routing rules can match on prefixes. The bridge is bidirectional: commands published on the Zenoh side are translated back into DDS writes the legacy robot understands.
Two operational details about the bridge deserve emphasis because they are where people lose hours. First, the bridge participates in the robot’s DDS domain as a full participant, which means it also sees — and re-announces — the discovery traffic of that domain. Run the bridge on a host with good connectivity to the robot’s DDS network and keep that network local; you do not want the bridge straddling a lossy link on the DDS side, because then you have reintroduced exactly the fragility Zenoh was meant to remove. Put the bridge close to the robot, and let Zenoh own the long haul. Second, the bridge exposes a configuration file of its own where you can allow- and deny-list the topics it mirrors. A real robot publishes dozens of topics you have no interest in shipping across a WAN — transform trees, diagnostics, internal debug streams — so curate that list rather than mirroring everything. The bridge will happily forward a full TF tree at 100 Hz if you let it, and that single oversight has sunk more than one teleop link. Treat the bridge’s allow-list as a first line of defence and the router’s per-link forwarding rules as a second.
A useful sanity check after the bridge comes up is to query the fabric from the native side. Because Zenoh exposes everything as queryable key expressions, you can list what the bridge is advertising and confirm the namespaces look right before you ever wire up a forwarding rule. If you see the robot’s topics appearing under the prefix you expected, the DDS-to-Zenoh translation is working; if they are missing, the problem is almost always a domain-ID mismatch on the -d flag rather than anything in the Zenoh layer.
Multi-robot namespacing
When several robots share a fabric, you keep them apart with ROS 2 namespaces, which Zenoh preserves as key-expression prefixes. Launch each robot under its own namespace:
# Robot one
ROS_NAMESPACE=warehouse/amr01 ros2 launch my_robot bringup.launch.py
# Robot two
ROS_NAMESPACE=warehouse/amr02 ros2 launch my_robot bringup.launch.py
Now amr01‘s scan topic becomes the key expression warehouse/amr01/scan and amr02‘s becomes warehouse/amr02/scan. A router rule that forwards warehouse/** carries the whole fleet; a rule scoped to warehouse/amr01/** carries just one robot. This prefix discipline is what makes Zenoh fleet routing tractable: instead of fighting DDS partitions, you express policy as key-expression globs.
The payoff of this scheme compounds as the fleet grows. With DDS you would reach for partitions or separate domain IDs to keep robots from cross-talking, and you would discover that domain IDs are a scarce, numeric, globally flat namespace that becomes a coordination headache the moment you have more than a handful of segregated groups. Zenoh’s hierarchical key expressions have no such ceiling: warehouse/zone-a/amr01, warehouse/zone-b/amr07, and lab/test-rig/amr99 all coexist in one fabric, and any routing or access-control rule can target any subtree with a glob. Want zone A’s robots visible to the zone-A supervisor but invisible to the office? Forward warehouse/zone-a/** to the supervisor’s router and nothing more. Want a fleet-wide health dashboard? Subscribe to warehouse/+/+/diagnostics and let the single-level wildcards fan in across every robot. This is policy-as-routing, and it scales because the structure lives in the topic namespace you already designed rather than in a separate, brittle layer of network configuration. Designing those namespaces deliberately on day one is the highest-leverage decision in a Zenoh fleet, because every forwarding and security rule you ever write will lean on them.
Connecting two sites over the WAN
Figure 3 shows the production shape: an edge router at the warehouse, an office router at the teleoperation desk, and a cloud relay both dial into.

Figure 3: A warehouse edge router and an office teleop router each connect outbound to a public cloud relay over mutual TLS, so neither site needs an inbound public IP.
The relay is the only host that needs a public, reachable endpoint; both sites connect out to it, so neither warehouse nor office needs inbound firewall holes or a static public IP. The operator station, running native rmw_zenoh, publishes velocity commands under teleop/** and subscribes to warehouse/** for camera and telemetry. Because routing is per-keyexpr, you can forward exactly those two prefixes across the WAN and nothing else — no stray parameter chatter, no discovery noise. That selective forwarding, combined with Zenoh’s TCP/QUIC transports that ride cleanly over the internet, is what delivers lossless WAN teleoperation where raw DDS would have drowned in retransmits.
In practice you express that selective forwarding with the router’s interceptor or downsampling configuration, but the simplest mental model is an allow-list of key-expression prefixes per link. The warehouse edge router is told to publish warehouse/** toward the relay and accept teleop/** coming back; the office router does the mirror. Everything outside those globs — node parameters, lifecycle transitions, debug topics a developer left running — stays local and never touches the WAN. This is a profound operational difference from tunnelling DDS, where you typically get an all-or-nothing bridge and then spend your evenings discovering that some chatty diagnostic topic is eating your uplink. With Zenoh the link budget is something you declare, not something you discover after it bites you. For a teleoperation link specifically, I forward the command topic with a reliable QoS so no velocity command is silently dropped, but forward the camera feed best-effort and, if the uplink is thin, apply a router-side downsampling rule so a 30 Hz image stream is throttled to a rate the link can actually sustain rather than queueing up stale frames. The operator sees current video and responsive control instead of a smooth-then-frozen feed, which is the failure mode that makes naive WAN teleop unusable.
Trade-offs, Gotchas, and What Goes Wrong
Figure 4 is the decision tree I reach for when topics refuse to cross a link.

Figure 4: Walk connectivity, keyexpr scope, QoS compatibility, and TLS trust in order — the first failing gate is almost always the cause of missing cross-site topics.
The first gotcha is the domain ID versus key-expression mismatch. DDS isolates traffic by numeric ROS_DOMAIN_ID; Zenoh isolates by key-expression scope. When you bridge, the bridge’s -d flag must match the robot’s domain, and the namespace it produces must match the prefixes your routers forward. A silent mismatch here is the single most common cause of “the bridge is up but I see nothing.” Second, QoS mismatches: ROS 2’s reliability and durability settings still apply, and
