ROS 2 DDS & RMW Guide 2026
Master ROS 2 middleware: switch between FastDDS, CycloneDDS, and Zenoh with RMW_IMPLEMENTATION, tune shared memory transport, isolate robot networks with ROS_DOMAIN_ID, and scale fleets with FastDDS Discovery Server.
Which RMW to Choose
FastDDS — default, best SROS2 support, most features, good for production robots
CycloneDDS — leaner, lower memory, easier config, popular with Nav2 and MoveIt
Zenoh — cross-network (cloud, VPN, browser), REST API, best for internet-connected robots
RMW — The Middleware Abstraction Layer
ROS 2 separates the robotics API (rclcpp/rclpy) from the underlying middleware via the RMW (ROS MiddleWare) interface. Swap DDS implementations without changing application code.
ROS 2 Middleware Architecture
═══════════════════════════════════════════════════════
Application code (rclcpp / rclpy)
│
▼
rcl (ROS Client Library — C)
│
▼
RMW interface (abstract API)
┌────┴────────────────────┐
│ │
▼ ▼
rmw_fastrtps_cpp rmw_cyclonedds_cpp
(eProsima FastDDS) (Eclipse CycloneDDS)
│ │
▼ ▼
DDS transport (UDP/TCP/Shared Memory)
Available RMW implementations:
rmw_fastrtps_cpp — FastDDS (eProsima), DEFAULT in Humble/Jazzy
rmw_cyclonedds_cpp — CycloneDDS (Eclipse), strong for small teams
rmw_zenoh_cpp — Zenoh (Zettascale), REST/WebSocket bridging
rmw_connextdds — Connext DDS (RTI), enterprise, commercial license
Change at runtime (no recompile needed):
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 run my_pkg my_node
Check current RMW:
ros2 doctor --report | grep RMW⚡ All nodes in a ROS 2 system must use the same RMW implementation to communicate — mixing FastDDS and CycloneDDS nodes on the same machine will result in them being invisible to each other.
Switch RMW with RMW_IMPLEMENTATION
Set the RMW_IMPLEMENTATION environment variable before launching nodes. Install the desired RMW package first.
# Install RMW packages
sudo apt install -y ros-humble-rmw-fastrtps-cpp # eProsima FastDDS (default)
ros-humble-rmw-cyclonedds-cpp # Eclipse CycloneDDS
ros-humble-rmw-zenoh-cpp # Zenoh (Humble+ backport)
# Switch to CycloneDDS for all nodes in this shell
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 run demo_nodes_cpp talker # now uses CycloneDDS
# Switch to Zenoh
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run demo_nodes_cpp talker
# Switch back to FastDDS (default)
unset RMW_IMPLEMENTATION
# or:
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
# Verify which RMW a running node is using
ros2 node info /talker # shows "RMW: rmw_cyclonedds_cpp"
# Set per-launch in a launch file
import os
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package="my_pkg",
executable="my_node",
additional_env={"RMW_IMPLEMENTATION": "rmw_cyclonedds_cpp"},
),
])FastDDS Configuration (XML Profile)
FastDDS behavior is configured via an XML file pointed to by FASTRTPS_DEFAULT_PROFILES_FILE. Common tweaks: shared memory transport, discovery server, and history depth.
<!-- fastdds_config.xml — common FastDDS tuning -->
<?xml version="1.0" encoding="UTF-8"?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<transport_descriptors>
<!-- Shared memory transport: zero-copy on same machine (~10x faster) -->
<transport_descriptor>
<transport_id>shm_transport</transport_id>
<type>SHM</type>
<maxMessageSize>1MB</maxMessageSize>
</transport_descriptor>
<!-- UDP transport with tuned buffer sizes -->
<transport_descriptor>
<transport_id>udp_transport</transport_id>
<type>UDPv4</type>
<sendBufferSize>1048576</sendBufferSize> <!-- 1 MB -->
<receiveBufferSize>4194304</receiveBufferSize> <!-- 4 MB -->
</transport_descriptor>
</transport_descriptors>
<participant profile_name="default_participant" is_default_profile="true">
<rtps>
<!-- Use SHM for local comms, UDP for cross-host -->
<userTransports>
<transport_id>shm_transport</transport_id>
<transport_id>udp_transport</transport_id>
</userTransports>
<useBuiltinTransports>false</useBuiltinTransports>
<!-- Discovery: reduce multicast if on large network -->
<builtin>
<discovery_config>
<leaseDuration><sec>30</sec></leaseDuration>
<leaseAnnouncement><sec>3</sec></leaseAnnouncement>
</discovery_config>
</builtin>
</rtps>
</participant>
</profiles>
<!-- Use it: -->
<!-- export FASTRTPS_DEFAULT_PROFILES_FILE=~/fastdds_config.xml -->⚡ Shared memory (SHM) transport gives the biggest speedup for large messages like camera images — benchmark shows 2-10x lower latency for /image_raw when publisher and subscriber are on the same machine.
CycloneDDS Configuration (CYCLONEDDS_URI)
CycloneDDS is configured with an XML file pointed to by CYCLONEDDS_URI, or inline via the env var itself. It uses less memory than FastDDS and is popular for resource-constrained robots.
# Create CycloneDDS config file
cat > ~/cyclone_config.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<CycloneDDS xmlns="https://cdds.io/config">
<Domain>
<!-- Restrict to specific network interface (e.g., robot's eth0) -->
<General>
<NetworkInterfaceAddress>eth0</NetworkInterfaceAddress>
<!-- Disable multicast if switch doesn't support it -->
<MulticastRecvNetworkInterfaceAddresses>eth0</MulticastRecvNetworkInterfaceAddresses>
</General>
<!-- Internal thread pool -->
<Internal>
<Threads>
<Thread name="recv">
<StackSize>4MB</StackSize>
</Thread>
</Threads>
</Internal>
<!-- Logging: only warnings and errors -->
<Tracing>
<Verbosity>warning</Verbosity>
</Tracing>
</Domain>
</CycloneDDS>
EOF
# Point to config file
export CYCLONEDDS_URI=file://~/cyclone_config.xml
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 run my_pkg my_node
# Or inline (small configs):
export CYCLONEDDS_URI='<CycloneDDS><Domain><General><NetworkInterfaceAddress>eth0</NetworkInterfaceAddress></General></Domain></CycloneDDS>'⚡ If ROS 2 nodes can't discover each other across a WiFi access point, the issue is often multicast not being forwarded between VLANs. Add a manual unicast peer: <Discovery><Peers><Peer address="192.168.1.100"/></Peers></Discovery> in the CycloneDDS config.
ROS_DOMAIN_ID — Network Isolation
ROS_DOMAIN_ID (0-232) partitions DDS discovery so multiple independent robot systems on the same network don't interfere with each other.
# Nodes in the same domain ID can see each other
# Nodes in different domain IDs are completely isolated
# Default domain = 0
# Set for robot #1 (e.g., AMR fleet unit 1)
export ROS_DOMAIN_ID=1
ros2 run my_pkg controller
# Set for robot #2 (same network, isolated from #1)
export ROS_DOMAIN_ID=2
ros2 run my_pkg controller
# Robot #1 cannot see robot #2's topics/services/actions at all
# (even on the same machine with the same RMW)
# ── Domain ID conventions ─────────────────────────────────────
# 0 — default, testing only
# 1-50 — production robots (one per robot)
# 100-130 — simulation environments
# 200-220 — CI/test environments
# (values 100-215 are "safe" — lower ports don't require root)
# ── Check which domain ID is in use ──────────────────────────
echo $ROS_DOMAIN_ID # empty = 0
ros2 doctor # shows "ROS Domain ID: 0"
# ── Localhost-only mode (ROS 2 Iron+) ────────────────────────
# Prevent nodes from broadcasting to the physical network at all
export ROS_LOCALHOST_ONLY=1
ros2 run my_pkg my_nodeFastDDS Discovery Server — Scale to Large Fleets
Default DDS uses multicast discovery which doesn't scale beyond ~20 nodes or across subnets. FastDDS Discovery Server replaces multicast with a centralized discovery service.
# Default discovery: every node multicasts to find peers
# Problem: scales poorly (~20 nodes), fails across subnets
# FastDDS Discovery Server: clients connect to a server for discovery
# Works across subnets, scales to thousands of nodes
# Step 1: Start the discovery server (one per system)
ros2 run rmw_fastrtps_cpp fastdds_discovery_server -l 0.0.0.0 # listen on all interfaces
-p 11811 # default port
# Step 2: Configure clients to use the server
# fastdds_ds_client.xml:
# <profiles>
# <participant profile_name="default_participant" is_default_profile="true">
# <rtps>
# <builtin>
# <discovery_config>
# <discoveryProtocol>CLIENT</discoveryProtocol>
# <discoveryServersList>
# <RemoteServer prefix="44.53.00.5f.45.50.52.4f.53.49.4d.41">
# <metatrafficUnicastLocatorList>
# <locator><udpv4><address>192.168.1.10</address><port>11811</port></udpv4></locator>
# </metatrafficUnicastLocatorList>
# </RemoteServer>
# </discoveryServersList>
# </discovery_config>
# </builtin>
# </rtps>
# </participant>
# </profiles>
export FASTRTPS_DEFAULT_PROFILES_FILE=~/fastdds_ds_client.xml
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
ros2 run my_pkg my_node⚡ For multi-robot fleets (10+ robots), Discovery Server reduces discovery traffic by 90%+ compared to multicast. Each robot connects to the server once; the server relays endpoint information only to nodes that need it.
Zenoh — Cross-Network and Web Bridging
Zenoh (rmw_zenoh_cpp) extends ROS 2 across networks where DDS UDP multicast fails: cloud, VPN, WebSocket to browsers, and REST APIs.
# Install Zenoh RMW
sudo apt install -y ros-humble-rmw-zenoh-cpp
# Start Zenoh router (required for cross-host communication)
zenohd # in one terminal, acts as the discovery/routing point
# Run nodes with Zenoh RMW
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run demo_nodes_cpp talker # in terminal 2
ros2 run demo_nodes_cpp listener # in terminal 3
# ── Cross-network: connect Zenoh routers ─────────────────────
# zenoh.json5 — connect to remote Zenoh router
# {
# "connect": {
# "endpoints": ["tcp/192.168.1.100:7447"]
# }
# }
ZENOH_ROUTER_CONFIG_URI=file:///home/user/zenoh.json5 zenohd
# ── REST API bridge (Zenoh built-in) ─────────────────────────
# GET http://localhost:8000/@/router/local/config/
# GET http://localhost:8000/scan (latest /scan message)
# PUT http://localhost:8000/cmd_vel (publish to /cmd_vel)
# Enable REST API in zenoh.json5:
# {
# "plugins": {
# "rest": { "http_port": 8000 }
# }
# }
# ── ros2-bridge for legacy ROS 1 ─────────────────────────────
# (separate from Zenoh — ros1_bridge package)
ros2 run ros1_bridge dynamic_bridge⚡ Zenoh's REST plugin lets web browsers and non-ROS services publish and subscribe to ROS 2 topics over HTTP — no ROS installation required on the client side. Useful for dashboards, remote monitoring, and cloud integrations.
Quick Reference
| Command / Concept | Details |
|---|---|
| Check RMW | ros2 doctor --report | grep RMW |
| Switch RMW | export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp (all nodes must match) |
| FastDDS config | export FASTRTPS_DEFAULT_PROFILES_FILE=~/fastdds_config.xml |
| CycloneDDS config | export CYCLONEDDS_URI=file://~/cyclone_config.xml |
| Domain isolation | export ROS_DOMAIN_ID=1 (0-232, default=0) |
| Localhost only | export ROS_LOCALHOST_ONLY=1 (Iron+, no network broadcast) |
| SHM transport | FastDDS: add SHM transport in XML profile for zero-copy on same machine |
| Discovery Server | ros2 run rmw_fastrtps_cpp fastdds_discovery_server -l 0.0.0.0 -p 11811 |
| Zenoh cross-net | export RMW_IMPLEMENTATION=rmw_zenoh_cpp + start zenohd router |
| Nodes not seeing each other | Check: same RMW? Same DOMAIN_ID? Multicast blocked? Try ROS_LOCALHOST_ONLY=1 for same-machine test |