QoS Profiles
Why QoS Exists
In ROS 1 a topic had one delivery behaviour: TCP, reliable, queued. ROS 2 runs on DDS, which parameterises delivery, and that parameterisation is exposed as a QoS profile on every publisher and subscriber.
This buys real capability. A camera stream can drop frames rather than buffer them, a latched map can be delivered to a subscriber that connects minutes later, and a wireless link can be told not to retry. It also introduces a failure mode that does not exist in ROS 1: two endpoints on the same topic with the same type that never exchange a message, because their profiles are incompatible.
The profile is fixed when the endpoint is created. There is no renegotiation, and no way to change it on a live publisher.
The Policies That Matter
Five of the policies come up in practice; the rest are rarely touched outside real-time work.
Reliability - RELIABLE - retry until acknowledged. Commands, transforms, configuration. - BEST_EFFORT - send once, accept loss. High-rate sensor data, anything over WiFi where a retry storm is worse than a dropped frame.
Durability - VOLATILE - a subscriber gets only what is published after it joins. - TRANSIENT_LOCAL - the publisher keeps the last depth messages and delivers them to late joiners. This is the ROS 2 equivalent of a ROS 1 latched topic, and it is how /map, /robot_description and static transforms reach nodes that start later.
History and depth - KEEP_LAST with depth N - a ring buffer of N. The normal choice. - KEEP_ALL - bounded only by middleware limits. Rarely correct; a slow subscriber turns it into unbounded memory growth.
Deadline, Liveliness and Lifespan - the ones to know about rather than use daily. DEADLINE declares a maximum gap between messages and fires a callback when it is missed, LIVELINESS detects a publisher that has stopped asserting it is alive, and LIFESPAN expires stale messages before delivery. Diagnostics and watchdogs are the usual consumers; see 08_Testing_Deployment_Ops/02_Service_Management_and_Diagnostics.ipynb.
The Built-in Profiles
rclpy.qos ships presets, and using the matching preset on both ends is the reliable way to avoid the incompatibility table below.
from rclpy.qos import (QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy,
qos_profile_sensor_data, qos_profile_system_default)
# Sensor data: best effort, depth 5. Use on both the driver and every consumer.
self.sub = self.create_subscription(LaserScan, "scan", self.cb, qos_profile_sensor_data)
# Latched configuration: late joiners get the last value.
latched = QoSProfile(depth=1,
reliability=ReliabilityPolicy.RELIABLE,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
history=HistoryPolicy.KEEP_LAST)
self.map_pub = self.create_publisher(OccupancyGrid, "map", latched)| Preset | Reliability | Durability | Depth | For |
|---|---|---|---|---|
default (an integer, e.g. 10) |
RELIABLE | VOLATILE | as given | general purpose |
qos_profile_sensor_data |
BEST_EFFORT | VOLATILE | 5 | lidar, camera, IMU |
qos_profile_services_default |
RELIABLE | VOLATILE | 10 | services |
qos_profile_parameters |
RELIABLE | VOLATILE | 1000 | parameter traffic |
| latched (hand-built, above) | RELIABLE | TRANSIENT_LOCAL | 1 | maps, robot_description |
Passing a bare integer is shorthand for the default profile with that depth, which is why most tutorial code interoperates: everything defaults to reliable and volatile.
The Incompatibility Table
Compatibility is one-directional. The rule is that the subscriber may not demand more than the publisher offers; a publisher offering more than the subscriber needs is always fine.
| Publisher offers | Subscriber requests | Match? | Symptom |
|---|---|---|---|
| BEST_EFFORT | RELIABLE | no | endpoints listed, zero messages |
| RELIABLE | BEST_EFFORT | yes | delivered, loss tolerated |
| VOLATILE | TRANSIENT_LOCAL | no | endpoints listed, zero messages |
| TRANSIENT_LOCAL | VOLATILE | yes | no history replay, live data fine |
| RELIABLE + VOLATILE | RELIABLE + VOLATILE | yes | the common default case |
| deadline 100 ms | deadline 50 ms | no | subscriber demands tighter timing |
| liveliness AUTOMATIC | liveliness MANUAL_BY_TOPIC | no | subscriber demands assertions |
History and depth are not part of matching. Mismatched depths always connect; they just buffer differently.
The failure is silent by design: DDS simply does not match the endpoints. What makes it diagnosable:
ros2 topic info /scan --verbose # prints every endpoint's full profile
ros2 doctor --report # QOS COMPATIBILITY LIST sectionros2 doctor --report names the incompatible pair and the offending policy directly, which is faster than comparing two profile dumps by eye. Both sides of the pair being visible in ros2 topic list while ros2 topic echo stays silent is the signature of this problem, as distinct from a discovery failure (where the endpoints do not appear at all, see ../07_Middleware_DDS/00_Discovery_and_RMW.ipynb) or executor starvation (where messages arrive but callbacks do not run, see 04_Executors_Lifecycle_and_Composition.ipynb).
One more asymmetry worth knowing: ros2 topic echo uses the default reliable profile, so it cannot subscribe to a best-effort-only publisher without --qos-reliability best_effort. An echo that prints nothing from a working sensor driver is usually this, not a broken driver.
Practical Rules
- Use the same preset on both ends.
qos_profile_sensor_dataon the publisher and a bare10on the subscriber is the incompatibility above, written by accident. - Latch anything a late joiner needs: maps, robot descriptions, static transforms, configuration. Otherwise startup order becomes load-bearing.
- Best effort for high-rate sensors, reliable for commands. A dropped lidar scan is replaced in 100 ms; a dropped stop command is not.
- Leave
KEEP_ALLalone unless there is a specific reason and a bounded producer. - Over WiFi or a VPN, prefer best effort for streams. Reliable delivery over a lossy link produces retry storms that degrade everything else on the link.
- Record what a topic’s profile is in the package README. There is no way to discover the intended profile from a type name, and a consumer written against the wrong guess fails silently.