Maker.io main logo

Intro to ROS Part 5: Services, Requests, and Responses with Python

2025-10-23 | By ShawnHymel

Single Board Computers Raspberry Pi SBC

In previous episodes, we learned how ROS 2 nodes communicate asynchronously using topics, where one node publishes messages and others subscribe to receive them. While this works well for broadcasting sensor readings or ongoing data streams, it’s not ideal for one-time, synchronous operations, such as requesting another node to perform a specific task. This is where ROS 2 services come into play.

Services in ROS 2 follow a request/response model, more akin to traditional client-server communication. One node acts as a service server, waiting to receive incoming requests. Another node acts as the client, sending a request and waiting for a response. This structure is ideal for use cases like setting parameters, triggering actions, or retrieving one-time data from another node. In this tutorial, you’ll learn how to implement both service servers and clients using Python in ROS 2.

In this tutorial, we will see how services work by creating a custom server and client in Python ROS nodes. You can watch a video version of this tutorial here:

The Docker image and code for this series can be found here: https://github.com/ShawnHymel/introduction-to-ros

What Are ROS 2 Services?

Intro to ROS Part 5: Services, Requests, and Responses with Python

Unlike topics, which are unidirectional and asynchronous, services are synchronous and strictly one-to-one. When a client sends a request, it expects a specific response from the server. These requests and responses are based on service interfaces, which define the structure of both the request and the response messages. For example, the example_interfaces/srv/AddTwoInts service defines two integers (a and b) as the request and one integer (sum) as the response.

It’s important to note that ROS 2 nodes can play multiple roles simultaneously. A single node can act as both a service client and a service server. This flexibility enables complex interactions in robotic systems, such as a central controller node that sends commands to subsystems and also provides telemetry data to a monitoring node.

A service interface in ROS 2 is defined in an .srv file and consists of two parts: the request and the response, separated by three dashes (---). For example, AddTwoInts.srv contains:

Copy Code
int64 a 
int64 b
 ---
int64 sum

This clearly outlines that the request contains two integers (a and b), and the response contains a single integer (sum). ROS 2 generates message classes from this interface, which you import and use in your Python code.

Let’s now build a simple example to see services in action using Python.

Creating the Service Server

Start by launching your ROS 2 Docker container and navigating to your workspace:

Copy Code
 docker run --rm -it -e PUID=$(wsl id -u) -e PGID=$(wsl id -g) -p 22002:22 -p 3000:3000 -v "${PWD}\workspace:/config/workspace" env-ros2

Inside your Python package (e.g., my_py_pkg), create a new file called minimal_server.py with the following code:

Copy Code
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class MinimalServer(Node):
 """Server example that adds two integers and responds with result""" 
def __init__(self):
 """Constructor"""
 # Call the Node class constructor with the node name
 super().__init__('minimal_server')
 # Create a service object
 self._srv = self.create_service(
   AddTwoInts,
  'add_ints',
   self._server_callback
 )
 def _server_callback(self, req, resp):
 """Responds with sum of request integers"""
 resp.sum = req.a + req.b
 self.get_logger().info(f"Received request: a={req.a}, b={req.b}")
 return resp 
def main(args=None):
 """Main entrypoint""" 
# Initialize and run node
 try:
 rclpy.init()
 node = MinimalServer()
 rclpy.spin(node)
 except (KeyboardInterrupt, ExternalShutdownException):
 pass
 finally:
    if node is not None:
 node.destroy_node()
 if rclpy.ok():
 rclpy.shutdown()
 if __name__ == '__main__':
 main()

We do not need to update package.xml, as we are using the same dependencies as last time. However, you do need to update setup.py to include the new executable. Under the minimal_subscriber executable list item, add:

Copy Code
"minimal_server = my_py_pkg.minimal_server:main",

Build and source your workspace:

Copy Code
colcon build --packages-select my_py_pkg

Then, run the server node:

Copy Code
source install/setup.bash 
ros2 run my_py_pkg minimal_server

In another terminal, you can list available services and call the one you just created:

Copy Code
ros2 service list 
ros2 service info /add_ints 
ros2 service call /add_ints example_interfaces/srv/AddTwoInts "{a: 10, b: 12}"

Intro to ROS Part 5: Services, Requests, and Responses with Python

Alternatively, you can use rqt:

Copy Code
rqt

Navigate to Plugins > Services > Service Caller, choose /add_ints, enter values, and click Call.

Intro to ROS Part 5: Services, Requests, and Responses with Python

Creating the Service Client

Create another file in your Python package called minimal_client.py:

Copy Code
import random 
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class MinimalClient(Node):
 """Client example that periodically calls the server"""
def __init__(self):
 "Constructor"""
# Call the Node class constructor with the node name
super().__init__('minimal_client')
# Create a client object
self._client = self.create_client(AddTwoInts, 'add_ints')
# Wait for service
while not self._client.wait_for_service(timeout_sec=2.0):
 self.get_logger().info('Waiting for service...')
# Periodically call method
self._timer = self.create_timer(2.0, self._timer_callback)
def _timer_callback(self):
 """Send request to server asking it to add two integers"""
# Fill out request message
req = AddTwoInts.Request()
req.a = random.randint(0, 10)
req.b = random.randint(0, 10)
# Send request to server and set callback
self.future = self._client.call_async(req) 
self.future.add_done_callback(self._response_callback)
def _response_callback(self, future):
 """Log result when received from server""" 
try:
  resp = future.result()
 self.get_logger().info(f"Result: {resp.sum}")
 except Exception as e:
 self.get_logger().error(f"{str(e)}")
def main(args=None):
 """Main entrypoint"""
# Initialize and run node
try: 
 rclpy.init()
 node = MinimalClient()
 rclpy.spin(node)
except (KeyboardInterrupt, ExternalShutdownException):
 pass
finally:
  if node is not None:
 node.destroy_node()
if rclpy.ok():
 rclpy.shutdown()
if __name__ == '__main__': 
main()

Update your setup.py again:

Copy Code
'console_scripts': [ 
… 
'minimal_server = my_py_pkg.minimal_server:main',
 'minimal_client = my_py_pkg.minimal_client:main',
 ]

When a client sends a request using call_async(), ROS 2 immediately returns a Future object. This object represents a placeholder for a result that hasn’t arrived yet. Instead of blocking the program, you can attach a function using add_done_callback() to automatically run when the result becomes available. This allows the node to continue processing other tasks without waiting, which is important in robotics where timing and responsiveness are critical.

Build and source:

Copy Code
colcon build --packages-select my_py_pkg

Then start the client:

Copy Code
source install/setup.bash 
ros2 run my_py_pkg minimal_client

With both the client and server running, you’ll see messages printed from both nodes. The server logs each request, and the client logs each response.

Intro to ROS Part 5: Services, Requests, and Responses with Python

Wrapping Up

In this tutorial, we introduced ROS 2 services and demonstrated how to use them in Python. You learned the difference between asynchronous topics and synchronous services, how to create a service server and client, and how to test services using both command-line tools and the rqt GUI. Services provide a powerful mechanism for request/response-style communication in robotic systems, ideal for triggering specific actions or retrieving immediate data.

Services are useful when you need reliable, point-to-point communication. Examples include requesting sensor calibration, initiating a mission, saving a snapshot, or querying a device's status. However, because services are synchronous, overuse can introduce delays. If many nodes need the same data or action, topics or actions (a related concept for long-running tasks) may be a better fit. It’s important to think through communication patterns when designing robotic systems to ensure scalability and responsiveness.

In the next episode, we’ll explore how to implement the same service architecture using C++. Stay tuned!

Mfr Part # 28009
EXPERIENTIAL ROBOTICS PLATFORM (
SparkFun Electronics
102,89 €
View More Details
Mfr Part # SC0194(9)
SBC 1.5GHZ 4 CORE 4GB RAM
Raspberry Pi
47,18 €
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.