RobotForge
Published·~12 min

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.

by RobotForge
#ros2#architecture#fundamentals

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_LOCAL durability); 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_LOCAL or 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.