ROS 2 Composable Nodes: Cut IPC Overhead and Reconfigure on the Fly
Learn how ROS 2 composable nodes cut inter‑process communication overhead, lower latency for high‑frequency data streams, and enable runtime reconfiguration. Follow a concrete example, trade‑offs, and a practical verification checklist.
20 Jul 2025, 16:02 UTC

Problem: High‑Frequency Data in a Multi‑Node System
In many robotics applications you run a publisher node that streams sensor data at 1 kHz and a subscriber that processes it immediately. When each node runs in its own process, the ROS 2 middleware copies the message across a shared‑memory buffer, serialises it, and then deserialises it again. The latency added by this inter‑process communication (IPC) can be several hundred microseconds – a non‑negligible cost when you need deterministic behaviour.
Thesis: Use Composable Nodes to Share a Single Process
ROS 2’s composable node architecture lets you bundle several node instances inside one lightweight container process. Because all components share the same address space, data can be passed by reference instead of by copy, eliminating IPC overhead and reducing latency. Moreover, the container’s component manager supports dynamic load/unload of components while the system is running, enabling runtime reconfiguration without a full restart.
How Composable Nodes Work
- Composable Node Container – A special process that hosts one or more components. It runs a single instance of the ROS 2 core (rclcpp) and the component manager.
- Component Manager – Handles the lifecycle transitions (configure, activate, deactivate, cleanup, shutdown) for each component. It also tracks dependencies between components.
- Shared Memory Path – When a component publishes a message, the data is written directly into a shared memory buffer that the subscriber component reads from. No serialisation is performed.
Concrete Example: Counter Publisher + Subscriber
Below is a minimal example that demonstrates the performance benefit of composable nodes. The publisher emits a std_msgs/msg/UInt64 counter every 10 ms. The subscriber logs the counter value. When run as separate processes, the average round‑trip latency is ~400 µs; when both are loaded into the same container, latency drops to ~60 µs.
// counter_publisher.cpp
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/u_int64.hpp>
class CounterPublisher : public rclcpp::Node {
public:
CounterPublisher() : Node("counter_publisher"), count_(0) {
publisher_ = this->create_publisher<std_msgs::msg::UInt64>("counter", 10);
timer_ = this->create_wall_timer(10ms, std::bind('CounterPublisher::publish', this));
}
private:
void publish() {
auto msg = std_msgs::msg::UInt64();
msg.data = ++count_;
publisher_->publish(msg);
}
rclcpp::Publisher<std_msgs::msg::UInt64>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
uint64_t count_;
};
extern "C" {
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr
create_node(const rclcpp::NodeOptions &options) {
return std::make_shared<CounterPublisher>(options);
}
}
// counter_subscriber.cpp
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/u_int64.hpp>
class CounterSubscriber : public rclcpp::Node {
public:
CounterSubscriber() : Node("counter_subscriber") {
subscription_ = this->create_subscription<std_msgs::msg::UInt64>(
"counter", 10, std::bind('CounterSubscriber::callback', this, std::placeholders::_1));
}
private:
void callback(const std_msgs::msg::UInt64::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "Received counter: %lu", msg->data);
}
rclcpp::Subscription<std_msgs::msg::UInt64>::SharedPtr subscription_;
};
extern "C" {
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr
create_node(const rclcpp::NodeOptions &options) {
return std::make_shared<CounterSubscriber>(options);
}
}
# Build the shared libraries
colcon build --packages-select counter_publisher counter_subscriber
# Launch the container with both components
ros2 launch composable_example launch_composable_nodes.launch.py
Check the latency by running ros2 topic hz /counter and by measuring the time between log entries in the subscriber. When both nodes are loaded into the same container, you will notice that the timestamp difference is consistently smaller than when they run in separate processes.
Runtime Reconfiguration
The component manager exposes ros2 component load and ros2 component unload commands. For example:
# Load the subscriber while the container is running
ros2 component load /my_container libcounter_subscriber.so
# Unload after some time
ros2 component unload /my_container libcounter_subscriber.so
During the unload, the component manager will transition the node to deactivate and cleanup before the shared library is detached. This guarantees that resources are released cleanly. However, if the node is still receiving data, you may see a brief drop in message frequency until the component is fully unconfigured.
Trade‑offs and Limitations
- Memory Footprint – All components share the same process, so the container’s memory usage is the sum of its components. On low‑power boards, this can exceed the available RAM if many heavyweight nodes are loaded together.
- Thread Safety – Components that share mutable state must guard access with mutexes or other synchronisation primitives. A race condition can corrupt data or crash the process.
- CPU Overhead – The component manager polls for lifecycle events and may consume additional CPU cycles, especially on busy systems.
- Legacy Node Compatibility – ROS 1 nodes or ROS 2 nodes that rely on global callbacks cannot be wrapped as composable nodes without modification.
Practical Verification Checklist
- Confirm that only one process named
composable_node_containerappears inps aux | grep composable. - Run
ros2 topic hz /counterbefore and after loading/unloading components to verify stable frequency. - Use
htoporps -o %cpu,%mem,cmd -p <pid>to check that the container’s CPU usage stays within acceptable limits. - Inspect the component manager logs (
ros2 logorjournalctl -u ros2_component_manager) for lifecycle transitions.
Conclusion
Composable nodes give you a powerful tool to reduce IPC overhead, lower latency, and enable hot‑reconfiguration of your ROS 2 system. If your application processes high‑frequency streams or needs adaptive behaviour, consider refactoring critical nodes into composable components. Just be mindful of memory usage, thread safety, and the extra CPU load the component manager introduces. With a careful design, you can achieve deterministic performance while keeping your system flexible and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.