Managing Robot Bring‑Up with ROS 2 Lifecycle Nodes
Avoid race conditions and deterministic startup in ROS 2 by orchestrating nodes with lifecycle states. A concrete example shows how a lidar driver and SLAM node can be launched in order, and the trade‑offs you’ll face.
25 May 2026, 21:01 UTC

The Problem: Race Conditions in Robot Bring‑Up
When a robot boots, dozens of nodes start almost simultaneously. If a perception node subscribes to a sensor topic before the sensor driver is ready, the subscription will drop the first few messages. In a larger system, this can cascade into missing data, controller failures, or even unsafe behavior. The core issue is that ROS 2 gives every node the same “ready” state – once the node is launched, it can start publishing or subscribing immediately, with no guarantee about the readiness of its peers.
Thesis: Explicit State Machines Make Startup Deterministic
ROS 2 lifecycle nodes expose a four‑step state machine: unconfigured, inactive, active, and finalized. By moving nodes through these states in a controlled order, you can guarantee that a publisher is live before a subscriber starts, or that a controller only activates after its hardware interface is ready. This blog shows how to wire a simple lidar driver and a SLAM node together using the lifecycle manager, and discusses the added complexity and how to mitigate it.
Lifecycle State Machine Overview
Each lifecycle node implements four callbacks:
on_configure– prepares resources but does not start publishing.on_activate– actually starts publishers/subscribers.on_deactivate– stops publishing but keeps resources.on_cleanup– releases resources and returns to unconfigured.
/node_name/transition (e.g., /lidar_driver/configure) moves the node between states. A node that never reaches active will not publish, preventing early subscriptions from missing data.
Building a Simple Lifecycle Driver
Below is a minimal C++ example that turns a standard ROS 2 publisher into a lifecycle node. The node reads a LIDAR scan and publishes it only after activation.
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_lifecycle/lifecycle_node.hpp>
class LidarDriver : public rclcpp_lifecycle::LifecycleNode {
public:
LidarDriver() : LifecycleNode("lidar_driver") {
publisher_ = this->create_publisher<sensor_msgs::msg::LaserScan>("/scan", 10);
}
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_configure(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(this->get_logger(), "Configured");
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_activate(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(this->get_logger(), "Activated");
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&LidarDriver::publish_scan, this));
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_deactivate(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(this->get_logger(), "Deactivated");
timer_.reset();
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
on_cleanup(const rclcpp_lifecycle::State &) override {
RCLCPP_INFO(this->get_logger(), "Cleaned up");
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
}
private:
void publish_scan() {
sensor_msgs::msg::LaserScan msg;
// Populate msg with sensor data – omitted for brevity
publisher_->publish(msg);
}
rclcpp::Publisher<sensor_msgs::msg::LaserScan>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};
#include <rclcpp_components/register_node_macro.hpp>
RCLCPP_COMPONENTS_REGISTER_NODE(LidarDriver)
Compile this into a component library and load it with a launch file. Notice that the publisher is created during construction but only starts sending data after on_activate.
Wiring with the Lifecycle Manager
The lifecycle_manager node (provided by ros2_control or ros2_lifecycle packages) keeps track of all lifecycle nodes and can issue transitions in a defined order. A typical launch file looks like this:
from launch import LaunchDescription
from launch_ros.actions import Node
ld = LaunchDescription()
# 1. Load the lidar driver component
ld.add_action(Node(
package='my_lidar_pkg',
executable='lidar_driver',
name='lidar_driver',
output='screen',
parameters=[{'use_sim_time': False}],
remappings=[('scan', '/scan')],
arguments=['--ros-args', '--log-level', 'info'],
respawn=False,
emulate_tty=True,
# The component is launched as a lifecycle node
node_executable='lidar_driver',
node_name='lidar_driver',
node_namespace='',
use_intra_process_comms=True,
# Indicate that this is a lifecycle node
node_type='component',
node_arguments=['--ros-args', '--log-level', 'info'],
# The component library must expose a lifecycle node
# The launch file will automatically treat it as such
))
# 2. Launch the SLAM node (also a lifecycle node)
ld.add_action(Node(
package='my_slam_pkg',
executable='slam_node',
name='slam_node',
output='screen',
parameters=[{'use_sim_time': False}],
remappings=[('scan', '/scan')],
arguments=['--ros-args', '--log-level', 'info'],
node_type='component',
))
# 3. Start the lifecycle manager
ld.add_action(Node(
package='lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager',
output='screen',
parameters=[{
'node_names': ['lidar_driver', 'slam_node'],
'autostart': True
}]
))
return ld
When the launch file runs, the manager will automatically send the configure transition to lidar_driver, then to slam_node. Because lidar_driver remains in the inactive state until the manager explicitly activates it, the SLAM node will never start publishing until after the lidar driver has moved to active. This guarantees that the SLAM node receives continuous data from the first message.
Running the Example
Assuming you have a ROS 2 Foxy workspace, build the packages, and source the install space:
colcon build --packages-select my_lidar_pkg my_slam_pkg
source install/setup.bash
ros2 launch my_robot_launch bringup.launch.py
Now observe the node states:
ros2 lifecycle get /lidar_driver state
# Expected output: current state: inactive
ros2 lifecycle get /slam_node state
# Expected output: current state: inactive
Activate the lidar driver manually to see the chain reaction:
ros2 lifecycle set /lidar_driver activate
# Now the lidar driver publishes /scan
ros2 lifecycle get /slam_node state
# Expected output: current state: active
ros2 topic echo /scan | head
Note that the SLAM node only starts publishing after the lidar driver is active. If you terminate the lidar driver, the SLAM node will automatically transition to inactive because it depends on the /scan topic.
Trade‑offs & Limitations
- Launch Complexity – You must write additional launch actions or a custom lifecycle manager to orchestrate transitions. This adds boilerplate and requires careful ordering.
- Transition Failures – If a node’s
on_configurecallback fails, the manager may leave the node in unconfigured. You need to handle timeouts or retry logic. - Version Dependence – Earlier ROS 2 releases (pre‑Foxy) lack some transition services. Ensure your distribution supports the full lifecycle API.
- Debugging Overhead – Inspecting node states requires
ros2 lifecycle getcommands or a custom UI. Adding state checks to your logs helps diagnose startup issues.
Despite these costs, the deterministic startup and clear failure points often outweigh the added complexity, especially in safety‑critical robots or when integrating many subsystems.
Actionable Closing
To adopt lifecycle nodes in your robot stack:
- Wrap each critical component (drivers, controllers, perception nodes) as a
LifecycleNode. - Use the built‑in
lifecycle_manageror a custom launcher to sequenceconfigureandactivatetransitions. - Add state checks to your launch scripts (e.g.,
ros2 lifecycle get) to confirm nodes reach the desired state before proceeding. - Monitor transition failures in logs and implement retry or fallback logic if needed.
With these steps, your robot’s bring‑up becomes predictable, reduces race conditions, and makes future maintenance easier. Happy hacking!
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.