Skip to main content
🌐 DDS/RMWROS 2 · June 2026

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

1

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.

text
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.

2

Switch RMW with RMW_IMPLEMENTATION

Set the RMW_IMPLEMENTATION environment variable before launching nodes. Install the desired RMW package first.

bash
# 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"},
        ),
    ])
3

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.

xml
<!-- 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.

4

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.

bash
# 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.

5

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.

bash
# 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_node
6

FastDDS 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.

bash
# 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.

7

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.

bash
# 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 / ConceptDetails
Check RMWros2 doctor --report | grep RMW
Switch RMWexport RMW_IMPLEMENTATION=rmw_cyclonedds_cpp (all nodes must match)
FastDDS configexport FASTRTPS_DEFAULT_PROFILES_FILE=~/fastdds_config.xml
CycloneDDS configexport CYCLONEDDS_URI=file://~/cyclone_config.xml
Domain isolationexport ROS_DOMAIN_ID=1 (0-232, default=0)
Localhost onlyexport ROS_LOCALHOST_ONLY=1 (Iron+, no network broadcast)
SHM transportFastDDS: add SHM transport in XML profile for zero-copy on same machine
Discovery Serverros2 run rmw_fastrtps_cpp fastdds_discovery_server -l 0.0.0.0 -p 11811
Zenoh cross-netexport RMW_IMPLEMENTATION=rmw_zenoh_cpp + start zenohd router
Nodes not seeing each otherCheck: same RMW? Same DOMAIN_ID? Multicast blocked? Try ROS_LOCALHOST_ONLY=1 for same-machine test