Skip to main content
🧩 CompositionROS 2 · June 2026

ROS 2 Composition Guide 2026

Run multiple nodes in a single process with rclcpp_components: create composable nodes, launch ComposableNodeContainer, load/unload components at runtime with ros2 component, and achieve zero-copy messaging with unique_ptr intra-process communication.

When to Use Composition

Use composition when nodes exchange high-bandwidth data (camera images, LiDAR point clouds) and latency matters. A camera driver + preprocessor + detector running in one container eliminates serialization overhead entirely — the raw Image buffer pointer is handed directly from publisher to subscriber without copying. For typical control nodes with string/float messages, the overhead savings are small and composition adds complexity without benefit.

1

What Is ROS 2 Composition?

Composition allows multiple rclcpp nodes to run in a single process, sharing the same executor and DDS participant. This reduces overhead and enables zero-copy intra-process communication.

text
ROS 2 Node Composition
═══════════════════════════════════════════════════════
Traditional (without composition):
  Process A  ────── DDS ──────  Process B
  [Node 1]   (network/IPC)      [Node 2]
  • Each process = separate executor + separate DDS participant
  • All messages serialized/deserialized through DDS transport
  • Higher latency and CPU overhead

With Composition:
  Single Process
  ┌──────────────────────────────────────┐
  │  Component Container                 │
  │  ┌────────┐  ┌────────┐  ┌────────┐ │
  │  │ Node 1 │  │ Node 2 │  │ Node 3 │ │
  │  └────────┘  └────────┘  └────────┘ │
  │       ↕ intra-process comms          │
  └──────────────────────────────────────┘
  • One executor, one DDS participant
  • Intra-process: zero-copy via shared_ptr / unique_ptr
  • External: still serialized through DDS (same as normal)

Key benefits:
  ✅ Zero-copy message passing between co-located nodes
  ✅ Lower latency (no DDS serialization overhead)
  ✅ Fewer OS processes to manage
  ✅ Runtime load/unload of nodes without restarting the container

Key classes:
  rclcpp_components — registration macro + Node factory
  rclcpp::executors::SingleThreadedExecutor / MultiThreadedExecutor
  ComponentManager   — manages loaded components in a container process

Composition only works with rclcpp (C++) natively. Python composable nodes exist via rclpy_components but are less common. For production performance-sensitive pipelines, prefer C++.

2

Creating a Composable Node (C++)

A composable node is a standard rclcpp::Node that registers itself with the component system via a macro. The key difference: use Node(options) not Node(name) to allow container-injected options.

cpp
// include/my_pkg/talker.hpp
#pragma once
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

namespace my_pkg {

class Talker : public rclcpp::Node {
public:
  // MUST take NodeOptions — not (const std::string & name)
  explicit Talker(const rclcpp::NodeOptions & options);

private:
  void timer_callback();
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
  rclcpp::TimerBase::SharedPtr timer_;
  size_t count_ = 0;
};

}  // namespace my_pkg

// ──────────────────────────────────────────────────────────
// src/talker.cpp
#include "my_pkg/talker.hpp"
#include <rclcpp_components/register_node_macro.hpp>

namespace my_pkg {

Talker::Talker(const rclcpp::NodeOptions & options)
: Node("talker", options)   // pass options to base class
{
  pub_ = create_publisher<std_msgs::msg::String>("chatter", 10);
  timer_ = create_wall_timer(
    std::chrono::milliseconds(500),
    std::bind(&Talker::timer_callback, this)
  );
}

void Talker::timer_callback() {
  auto msg = std_msgs::msg::String{};
  msg.data = "Hello " + std::to_string(count_++);
  pub_->publish(std::move(msg));
}

}  // namespace my_pkg

// Register as a component — this generates the factory function
// that ComponentManager uses to instantiate the class
RCLCPP_COMPONENTS_REGISTER_NODE(my_pkg::Talker)

The NodeOptions argument is mandatory for composable nodes. The container injects remappings, parameters, and intra-process settings through NodeOptions at load time.

3

CMakeLists.txt for a Component Library

Composable nodes are built as shared libraries, not executables. The ament_target_dependencies and rclcpp_components_register_nodes calls wire the factory into the plugin system.

cmake
cmake_minimum_required(VERSION 3.8)
project(my_pkg)

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(std_msgs REQUIRED)

# ── Build as SHARED library (not executable) ────────────────
add_library(talker_component SHARED src/talker.cpp)

ament_target_dependencies(talker_component
  rclcpp
  rclcpp_components
  std_msgs
)

# Register component(s) in this library
rclcpp_components_register_nodes(talker_component
  "my_pkg::Talker"
)

# ── Optional standalone executable (for development) ───────
add_executable(talker_node src/talker_main.cpp)
target_link_libraries(talker_node talker_component)

# ── Install ─────────────────────────────────────────────────
install(TARGETS talker_component talker_node
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION lib/${PROJECT_NAME}
)

ament_package()

# ──────────────────────────────────────────────────────────
# src/talker_main.cpp  (standalone wrapper)
// int main(int argc, char * argv[]) {
//   rclcpp::init(argc, argv);
//   rclcpp::NodeOptions options;
//   auto node = std::make_shared<my_pkg::Talker>(options);
//   rclcpp::spin(node);
//   rclcpp::shutdown();
// }
4

Launching a Component Container

The component_container is a pre-built ROS 2 executable that hosts any number of composable nodes. Launch files use ComposableNodeContainer and ComposableNode to declare which components load at startup.

python
# launch/composition_launch.py
from launch import LaunchDescription
from launch_ros.actions import ComposableNodeContainer, Node
from launch_ros.descriptions import ComposableNode


def generate_launch_description():

    # ── ComposableNodeContainer: hosts multiple components ──
    container = ComposableNodeContainer(
        name="my_container",
        namespace="",
        package="rclcpp_components",
        # Use component_container_mt for MultiThreadedExecutor
        executable="component_container",
        composable_node_descriptions=[
            ComposableNode(
                package="my_pkg",
                plugin="my_pkg::Talker",
                name="talker",
                parameters=[{"publish_rate": 10.0}],
                remappings=[("chatter", "/robot/chatter")],
                # Enable intra-process comms for this component
                extra_arguments=[{"use_intra_process_comms": True}],
            ),
            ComposableNode(
                package="my_pkg",
                plugin="my_pkg::Listener",
                name="listener",
                extra_arguments=[{"use_intra_process_comms": True}],
            ),
        ],
        output="screen",
    )

    # Normal nodes can coexist in the same launch file
    tf_broadcaster = Node(
        package="tf2_ros",
        executable="static_transform_publisher",
        arguments=["0", "0", "0.5", "0", "0", "0", "base_link", "camera_link"],
    )

    return LaunchDescription([container, tf_broadcaster])

component_container uses SingleThreadedExecutor. Use component_container_mt for MultiThreadedExecutor (required with ReentrantCallbackGroups). Use component_container_isolated for a dedicated thread per component.

5

Loading and Unloading Components at Runtime

The ros2 component CLI lets you load, unload, and list components in a running container without restarting it — powerful for dynamic reconfiguration.

bash
# ── Start an empty container ───────────────────────────────
ros2 run rclcpp_components component_container

# In another terminal:

# Load a component into the running container
ros2 component load /ComponentManager my_pkg my_pkg::Talker

# Load with parameters
ros2 component load /ComponentManager my_pkg my_pkg::Talker   --param "publish_rate:=5.0"

# Load with name remapping
ros2 component load /ComponentManager my_pkg my_pkg::Talker   --node-name my_talker   --remap chatter:=/robot/chat

# List all loaded components
ros2 component list
# Output:
# /ComponentManager
#   1  /talker
#   2  /listener

# Unload component by ID (from ros2 component list)
ros2 component unload /ComponentManager 1

# ── Programmatic loading (C++ client) ────────────────────
# In a launch file or node:
# rclcpp::Client<composition_interfaces::srv::LoadNode>::SharedPtr load_client;
# auto request = std::make_shared<composition_interfaces::srv::LoadNode::Request>();
# request->package_name = "my_pkg";
# request->plugin_name = "my_pkg::Talker";
6

Zero-Copy Intra-Process Communication

When both publisher and subscriber are in the same process, use unique_ptr messages to pass ownership without copying. This is the main performance advantage of composition.

cpp
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <rclcpp_components/register_node_macro.hpp>

namespace my_pkg {

class ImageProcessor : public rclcpp::Node {
public:
  explicit ImageProcessor(const rclcpp::NodeOptions & options)
  : Node("image_processor", options)
  {
    // ── Intra-process publisher: publish unique_ptr ───────
    // Zero-copy: ownership transfers to subscriber directly
    pub_ = create_publisher<sensor_msgs::msg::Image>(
      "/processed/image", 10
    );

    // ── Subscriber receives unique_ptr (not shared_ptr) ───
    // unique_ptr callback = take ownership (zero-copy guaranteed)
    sub_ = create_subscription<sensor_msgs::msg::Image>(
      "/raw/image", 10,
      [this](sensor_msgs::msg::Image::UniquePtr msg) {
        // msg moved in — modify in-place, no copy
        msg->header.stamp = now();
        pub_->publish(std::move(msg));   // zero-copy publish
      }
    );
  }

private:
  rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr pub_;
  rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_;
};

}  // namespace my_pkg

RCLCPP_COMPONENTS_REGISTER_NODE(my_pkg::ImageProcessor)

// In launch file or code:
// extra_arguments=[{"use_intra_process_comms": True}]
// ↑ MUST set this for both publisher and subscriber nodes

For zero-copy to work: (1) Both nodes in the same container, (2) use_intra_process_comms=True on BOTH nodes, (3) subscriber callback takes unique_ptr<Msg> not const Msg&. If any condition fails, messages are copied as normal.

7

Python Composable Nodes

Python composable nodes exist via rclpy_components but are not zero-copy. They are useful for dynamic loading in heterogeneous systems.

python
# Python composable node — uses rclpy_components (not common)
# Most production use cases: use C++ for composable nodes

# setup.py entry_points:
# 'rclpy_components': ['my_listener = my_pkg.listener:main']

# my_pkg/listener.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class Listener(Node):
    def __init__(self):
        super().__init__("listener")
        self.sub = self.create_subscription(
            String, "chatter", self._cb, 10
        )

    def _cb(self, msg):
        self.get_logger().info(f"Heard: {msg.data}")


def main(args=None):
    rclpy.init(args=args)
    node = Listener()
    rclpy.spin(node)
    rclpy.shutdown()

# In launch file:
# from launch_ros.descriptions import ComposableNode
# ComposableNode(
#     package="my_pkg",
#     plugin="my_pkg.listener.Listener",  # Python dotted path
#     name="listener",
# )
8

Debugging Composition

Use ros2 component and ros2 node commands to inspect running containers and loaded components.

bash
# List all component managers in the system
ros2 component list

# Show container and all loaded components with types
ros2 component list -v

# Introspect nodes loaded in a specific container
ros2 node list
# → /my_container  (the container itself)
# → /talker        (loaded component)
# → /listener      (loaded component)

# Show node parameters (parameters are scoped per component)
ros2 param list /talker
ros2 param get /talker publish_rate

# Check intra-process communication is active
# Look for 'intra_process_comms' in node info
ros2 node info /talker
# Topics showing "intra-process" = zero-copy path active

# Verify shared library is registered correctly
ros2 pkg executables rclcpp_components
# → component_container
# → component_container_isolated
# → component_container_mt

# Check component plugin exports
ros2 pkg executables my_pkg
colcon info my_pkg | grep -i component

Quick Reference

ConceptDetails
Composable node baseextends rclcpp::Node, takes NodeOptions, RCLCPP_COMPONENTS_REGISTER_NODE
Build targetadd_library(X SHARED) not add_executable — must be a shared library
component_containerSingleThreadedExecutor container — use _mt for multi-threaded
ComposableNodeContainerLaunch file action that starts container + loads components
Runtime loadros2 component load /ComponentManager pkg plugin_name
Runtime unloadros2 component unload /ComponentManager <id>
Zero-copy requirementuse_intra_process_comms=True on ALL nodes + unique_ptr callbacks
List componentsros2 component list (shows container → component IDs)
Python composablerclpy_components — supported but NOT zero-copy
component_container_isolatedEach component in its own thread — isolation without separate processes