Services vs actions vs topics: when to use which
Topics for streams, services for request-response, actions for long-running goals. A decision tree with concrete examples and the failure modes of picking wrong.
ROS 2 offers three communication primitives. Ninety percent of bugs in new ROS codebases come from picking the wrong one. The good news: the decision is almost always obvious once you know the decision tree.
The decision tree
- Is the data a continuous stream (sensor readings, commands, state)? → Topic
- Is it a quick question that needs an immediate answer? → Service
- Is it a long-running request that needs feedback and can be cancelled? → Action
That's 95% of real-world decisions. The other 5% is edge cases we'll cover at the end.
Topics: continuous streams
Use when: the sender doesn't know or care who's listening, and the same information flows over and over.
Good topic examples:
/odom— odometry at 50 Hz/cmd_vel— velocity commands at 20 Hz/scan— lidar sweeps at 10 Hz/battery_state— power telemetry at 1 Hz
Failure mode: using topics for one-shot events like "the planner finished." The subscriber might miss the moment. Use a service or latch the topic (TRANSIENT_LOCAL) instead.
Services: request-response
Use when: caller needs a specific answer to a specific question, now.
Good service examples:
/get_map— return the current occupancy grid/set_mode— switch a driver from "manual" to "autonomous"/calibrate— run a one-shot IMU calibration and return the offsets
Services are synchronous in the sense that the caller waits for a reply, but they should be fast. If the work takes more than a second, you probably want an action instead.
Failure mode: using a service for a long-running job like "drive to waypoint." The client blocks, can't cancel, can't see progress. Users hate it. Use an action.
Actions: long-running goals with feedback
An action is the ROS 2 primitive for jobs that:
- Take time (seconds to hours)
- Can fail, be cancelled, or be preempted
- Want to report progress while running
Under the hood, an action is built from three topics and two services, wrapped in one tidy API. You get a goal (start the task), feedback (periodic updates), and a result (final outcome), plus the ability to cancel.
Good action examples:
/navigate_to_pose— drive to a goal, report distance remaining, allow cancel/move_group— plan and execute an arm trajectory/dock— dock to the charging station, report alignment progress/follow_joint_trajectory— the standard interface for executing a planned trajectory over time
Failure mode: using an action for everything. The boilerplate cost is real — if the job completes in a millisecond, use a service.
A worked example: navigating to a waypoint
The naïve (wrong) design: a topic /nav_goal where the planner publishes goals; a topic /nav_status where the controller publishes state. Problems: Can't tell if a goal was accepted. Can't cancel. Two clients publishing to /nav_goal race each other.
The right design (this is what Nav2 actually does): an action /navigate_to_pose. The client sends a goal (PoseStamped). The server responds with accept/reject. While driving, it publishes feedback (distance remaining). When done, it returns success, failure, or cancelled. Two clients queue up instead of racing.
The 5% edge cases
- Latched topic vs service. If you want "ask for the current map," both work. Latched topic is fire-and-forget (
TRANSIENT_LOCALdurability); service is explicit. Prefer a service for clear semantics unless you have many clients. - Parameter vs service. Configuration that changes rarely — prefer ROS parameters. They come with their own change-notification mechanism.
- Event vs topic. ROS 2 doesn't have first-class events; a topic with
TRANSIENT_LOCALor a service is the usual substitute.
How each appears in ros2 CLI
ros2 topic list # all topics
ros2 topic echo /odom # watch messages
ros2 service list # all services
ros2 service call /get_map nav_msgs/srv/GetMap "{}"
ros2 action list # all actions
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "..."
When you're debugging, run these in this order. Most confusion clears up once you see what primitive was actually used versus what you expected.
Rule of thumb
If you're not sure, start with a topic. It's the cheapest primitive and easiest to refactor. Move up to service, then action, only when the topic model breaks down. Over-engineering the primitive layer is its own bug factory.
Comments
Sign in to post a comment.