ROS 2 reference
QoS & DDS
The most confusing class of ROS 2 bug is the one that throws no error. Two nodes agree on a topic name, both show up in the graph, and yet no data crosses. The cause is almost always a QoS mismatch the DDS layer resolved by silently refusing to connect.
Why two nodes never talk
Quality of Service, QoS, is a set of per-topic policies enforced by the DDS layer that sits under ROS 2. A publisher and a subscriber only connect if their QoS profiles are compatible. If the profiles are incompatible there is no exception, no crash, and no connection: the subscriber simply receives nothing.
This is the single most confusing class of ROS 2 bug because everything looks healthy. ros2 topic list shows the topic, both endpoints exist, and the names match. The negotiation that failed happened one layer down, in DDS, and it failed quietly.
The policies that matter
Reliability
RELIABLE retries until every message is delivered. BEST_EFFORT is fire and forget and may drop messages under load. Sensor streams use BEST_EFFORT because a stale scan is worthless anyway. Commands use RELIABLE because a dropped command is a real fault.
Durability
VOLATILE delivers only messages published after the subscriber joins. TRANSIENT_LOCAL makes the publisher latch the last message so a late-joining subscriber still gets it. /map and /tf_static are TRANSIENT_LOCAL, which is why a node that starts late still receives them.
History
KEEP_LAST is a ring buffer of depth N, and KEEP_ALL keeps everything the middleware can hold. The depth is the queue size, how many outgoing or incoming messages are buffered before the oldest is dropped.
Deadline, Liveliness, and Lifespan policies also exist and can affect compatibility, but they show up far less often than the three above.
The compatibility rules
DDS matches endpoints with a request-versus-offered rule, often written RxO. The publisher offers a level of quality and the subscriber requestsone. The rule is that the publisher’s offered quality must be greater than or equal tothe subscriber’s requested quality.
For reliability, a RELIABLE subscriber requires a RELIABLE publisher. A BEST_EFFORT subscriber accepts either. So a RELIABLE subscriber with a BEST_EFFORT publisher is incompatible and moves no data.
For durability, a TRANSIENT_LOCAL subscriber requires a TRANSIENT_LOCAL publisher. A VOLATILE subscriber accepts either. So a TRANSIENT_LOCAL subscriber with a VOLATILE publisher is incompatible.
The failures, and their tells
- Topic exists, both endpoints listed, echo shows nothing
- Reliability or durability mismatch. The endpoints never negotiated a connection, so nothing is ever delivered.
- A late subscriber misses a latched message (/map, /tf_static empty)
- Publisher is VOLATILE but the subscriber needs TRANSIENT_LOCAL, or vice versa. Latching requires TRANSIENT_LOCAL on both sides.
- Sensor data stutters or lags under load over WiFi
- RELIABLE QoS on a high-rate sensor stream forces retransmits and backpressure. Sensor data should be BEST_EFFORT.
- ros2 topic echo works but your node gets nothing
- The CLI subscribes with a compatible default profile while your node requested an incompatible one. Match the publisher.
- Rewriting a node fixed it but you do not know why
- You changed the QoS profile to match. Read the offered profile and set yours compatible on purpose, not by accident.
Debugging QoS
Read the actual profiles
You cannot reason about a mismatch you have not read. The verbose topic info prints the full QoS of every endpoint plus its counts, so you can compare offered against requested side by side.
# Verbose topic info prints the QoS of every endpoint plus counts
ros2 topic info /scan --verbose
# Look for Reliability, Durability, History on each publisher and subscriber.
# Endpoint counts of pub:1 sub:1 but no data = incompatible profiles.Common preset mismatch
The rclpy and rclcpp default profile is RELIABLE + VOLATILE + KEEP_LAST depth 10. The sensor_data preset is BEST_EFFORT + VOLATILE + KEEP_LAST depth 5. A subscriber left on the default profile listening to a sensor_data publisher is a RELIABLE subscriber against a BEST_EFFORT publisher, which is incompatible and receives nothing. Subscribe with the same profile the sensor publishes with.
from rclpy.qos import qos_profile_sensor_data
# Subscribe with the SAME profile the sensor publishes with
self.create_subscription(LaserScan, '/scan', cb, qos_profile_sensor_data)Fix it in a lab
A dropped-message or silent-topic case is a QoS bug, and the only way to build the reflex is to diagnose one in a real container where the logs, the profiles, and the missing data are all in front of you. Open a lab and find the mismatch yourself.