Choosing Between ROS 2 Services and Actions for Long‑Running Tasks
Learn when to use a ROS 2 Service versus an Action, see a concrete example, and understand the trade‑offs in complexity and blocking behavior.
08 Jan 2026, 15:52 UTC

Problem: You need a robot to perform a task that may take seconds or minutes
Imagine a mobile robot that must navigate to a goal pose while avoiding obstacles. The navigation stack could take an indeterminate amount of time, and the client that issued the goal may want to:
- Receive periodic progress updates (e.g., distance remaining)
- Cancel the navigation if a higher‑priority command arrives
If you model this interaction as a simple request‑response, the client would block until the navigation finishes, making cancellation impossible and wasting CPU cycles while waiting.
Thesis: Use ROS 2 Actions for long‑running, cancellable work; reserve Services for short, synchronous exchanges
The decision hinges on two questions:
- Does the caller need intermediate feedback or the ability to pre‑empt the operation?
- Is the operation expected to complete quickly (typically < 100 ms) without blocking the caller’s thread?
If you answer “yes” to the first question, an Action is the appropriate pattern. If you answer “no” to both, a Service is simpler and cheaper.
How Services work
A ROS 2 Service is defined by a single pair of topics: a request topic and a response topic (hidden from the user). The server executes the callback and returns a response before the call returns. Unless you use an asynchronous client, the calling thread is blocked for the duration of the callback.
# Example: a simple addition service (C++)
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
class AddTwoIntsServer : public rclcpp::Node {
public:
AddTwoIntsServer() : Node("add_two_ints_server") {
service_ = this->create_service(
"add_two_ints",
std::bind(&AddTwoIntsServer::add, this, std::placeholders::_1, std::placeholders::_2));
}
private:
void add(const std::shared_ptr request_header,
const std::shared_ptr request,
std::shared_ptr response) {
(void)request_header;
response->sum = request->a + request->b;
}
rclcpp::Service::SharedPtr service_;
};
int main(int argc, char ** argv) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared());
rclcpp::shutdown();
return 0;
}
Calling this service from a client blocks until the sum is computed.
How Actions work
An Action builds on three underlying topics: goal, feedback, and result. The client sends a goal, can receive periodic feedback, and may request cancellation at any time. The server implements a state machine that handles goal acceptance, execution, and result reporting.
# Example: a simple timer action (Python)
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from my_action_interfaces.action import Timer
class TimerActionServer(Node):
def __init__(self):
super().__init__('timer_action_server')
self._action_server = ActionServer(
self,
Timer,
'timer',
self.execute_callback)
async def execute_callback(self, goal_handle):
self.get_logger().info('Executing timer goal')
feedback_msg = Timer.Feedback()
for i in range(1, goal_handle.request.duration + 1):
if goal_handle.is_cancel_requested:
goal_handle.cancel()
self.get_logger().info('Goal canceled')
return Timer.Result()
feedback_msg.elapsed_time = i
goal_handle.publish_feedback(feedback_msg)
await rclpy.sleep(1.0)
goal_handle.succeed()
result = Timer.Result()
result.total_time = goal_handle.request.duration
return result
def main(args=None):
rclpy.init(args=args)
rclpy.spin(TimerActionServer())
rclpy.shutdown()
if __name__ == '__main__':
main()
Here the client can:
- Send a goal with a desired duration
- Receive feedback each second
- Cancel the timer mid‑execution
Worked example: Navigation stack
Suppose you want to trigger a navigation goal from a higher‑level behavior node.
- Service approach: Define a service
NavigateToPosethat takes a pose and returns a boolean success. The behavior node calls the service and blocks until the navigation finishes or fails. If the navigation takes 30 seconds, the behavior node’s thread is unavailable for other work, and you cannot abort the navigation without killing the node. - Action approach: Use the existing
nav2_msgs/action/NavigateToPoseaction. The behavior node sends a goal, receives periodicfeedback(e.g., distance to goal), and can callcancel_goalif a new higher‑priority task arrives. The navigation server runs in its own thread pool, leaving the behavior node free to process other events.
To verify the difference in topic count, run:
# List topics before starting the node
ros2 topic list
# Start the service server
ros2 run my_pkg add_two_ints_server
# List topics again – you will see only the service’s request/response topics
ros2 topic list
# Stop the server, start the action server
ros2 run my_pkg timer_action_server
# List topics – you will notice three topics: timer/_goal, timer/_feedback, timer/_result
ros2 topic list
No output is invented; you will observe the actual topics present on your system.
Trade‑offs and limitations
- Actions add overhead: Three topics mean more memory usage and more messages on the ROS graph. For very short operations (< 10 ms) this overhead can outweigh the benefits.
- Services can block: If you must use a Service for a longer task, spin the server in a MultiThreadedExecutor or use an asynchronous client to avoid blocking the caller’s thread.
- Complexity: Implementing an Action requires handling goal acceptance, cancellation, and feedback loops, which is more code than a simple Service callback.
Actionable closing
When designing a new ROS 2 node, ask:
- Do I need progress reports or the ability to stop the operation?
- Is the expected duration short enough that blocking is acceptable?
- If the answer to (1) is yes, choose an Action; otherwise, a Service is sufficient.
Start with a Service for simple get/set or configuration‑style interactions. Refactor to an Action only when you observe the need for feedback or cancellation during testing. This approach keeps your ROS graph lightweight while giving you the flexibility to scale up when the application demands it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.