ROS2 QoS settings explained the wrong way sound like an abstract list of enums. Explained the right way, they explain a specific, maddening bug: a subscriber node that starts cleanly, shows up in ros2 node info, is definitely subscribed to the right topic, and yet never receives a single message. No error, no warning, no crash. Just silence.
This happens because ROS2 runs on DDS, and DDS treats reliability, durability, and history as a contract that both sides of a connection must agree on. When they disagree, DDS does not connect the two endpoints at all. It fails silently by design.
The three QoS policies that matter most
ROS2 exposes a longer list of QoS policies, but three cause almost all of the practical pain.
Reliability
- Reliable: the publisher guarantees delivery. It keeps a history buffer and retransmits samples the subscriber has not acknowledged, using periodic heartbeat and ACKNACK messages under the hood.
- Best effort: the publisher sends once and does not retry. If a packet is dropped on the wire, it is gone.
Durability
- Volatile: a late-joining subscriber gets nothing that was published before it connected.
- Transient local: the publisher holds the last N samples and delivers them to any subscriber that connects afterward, even if it joins late.
History
- Keep last (depth N): only the most recent N samples are buffered.
- Keep all: every sample is buffered up to the resource limits of the DDS implementation.
Each policy individually is easy to understand. The part that trips people up is that ROS2 enforces QoS compatibility between a publisher and a subscriber before it will let them talk at all.
The compatibility rule that causes the bug
The rule for reliability is one-directional: a publisher offering reliable can serve a subscriber that requests reliable or best effort. But a publisher offering best effort cannot serve a subscriber that requests reliable. The subscriber is asking for a delivery guarantee the publisher never promised, so DDS refuses to match them.
This is exactly the situation ROS2's built-in sensor data profile creates if you are not paying attention. A LiDAR driver node commonly publishes on rclcpp::SensorDataQoS() or the Python equivalent, which sets reliability to best effort and a small keep-last depth, on the reasonable assumption that a dropped scan is better than a late one. If you then write a subscriber using the ROS2 default QoS, which is reliable, the two profiles are incompatible. The subscription is created, the node runs, no exception is thrown, and no data ever arrives.
Diagnosing it
The fastest way to confirm a QoS mismatch is ros2 topic info with the verbose flag, which prints the QoS profile of every publisher and subscriber on a topic:
ros2 topic info /scan --verbose
Type: sensor_msgs/msg/LaserScan
Publisher count: 1
Node name: lidar_driver
Node namespace: /
Topic type: sensor_msgs/msg/LaserScan
Endpoint type: PUBLISHER
GID: 01.0f.7a...
QoS profile:
Reliability: BEST_EFFORT
Durability: VOLATILE
Lifespan: Infinite
Deadline: Infinite
Liveliness: AUTOMATIC
Liveliness lease duration: Infinite
History (Depth): KEEP_LAST (5)
Subscription count: 1
Node name: scan_filter
Node namespace: /
Topic type: sensor_msgs/msg/LaserScan
Endpoint type: SUBSCRIPTION
GID: 01.0f.7b...
QoS profile:
Reliability: RELIABLE
Durability: VOLATILE
Lifespan: Infinite
Deadline: Infinite
Liveliness: AUTOMATIC
Liveliness lease duration: Infinite
History (Depth): KEEP_LAST (10)
The publisher is BEST_EFFORT, the subscription is RELIABLE. That single line pair is the entire bug. ros2 topic hz /scan run from the subscriber's process context would report nothing, while the same command run against the publisher directly would show data flowing fine, confirming the break is at the QoS boundary and not the driver.
Fixing the mismatch
There are two valid fixes, and which one is correct depends on what the data actually needs.
If occasional dropped messages are acceptable, which is true for most sensor streams, match the subscriber to best effort:
import rclpy
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=5,
)
self.create_subscription(LaserScan, '/scan', self.scan_callback, qos)
If every sample genuinely matters, for example a command topic where a missed message means a joint never gets its target, change the publisher to reliable instead:
from rclpy.qos import QoSProfile, ReliabilityPolicy
qos = QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE)
self.create_publisher(JointCommand, '/joint_cmd', qos)
Do not default to reliable everywhere out of caution. Reliable delivery adds retransmission overhead and, under sustained packet loss or a slow subscriber, can cause the publisher's history buffer to back up and add latency instead of removing it. For high-rate sensor data, best effort with a shallow keep-last depth is usually the right choice precisely because a stale retransmitted scan is less useful than the next fresh one.
Durability: the other silent failure
A second, less common version of this bug involves durability instead of reliability. A map server that publishes the occupancy grid once at startup on a transient local publisher will correctly deliver that map to any subscriber using transient local, including ones that start later. But a subscriber left on the default volatile durability will never see that one-time message if it starts after the publish happened, because volatile subscribers only receive samples published while they were already connected.
The fix is the same shape as the reliability case: match the subscriber's durability to what the publisher actually offers, using QoSProfile(durability=DurabilityPolicy.TRANSIENT_LOCAL, depth=1) for consumers of one-shot or slowly-changing state like a static map or a URDF-derived transform.
A quick pre-flight check
Before assuming a driver, network, or callback scheduling bug (see ROS2 executors and callback groups explained for that class of failure), run ros2 topic info <topic> --verbose on any topic where a subscriber reports zero messages. If the publisher and subscriber show different reliability or durability values, that is the entire bug, and it is a one-line QoS profile fix rather than a debugging session.
- Confirm the publisher exists and is actually publishing with
ros2 topic hzrun against it directly. - Run
ros2 topic info --verboseand compare reliability and durability on both sides. - Match the subscriber's QoS to the publisher's intent, not the ROS2 default.
- Re-check with
ros2 topic hzfrom the subscriber's side to confirm data now flows.
QoS mismatches are one of the few ROS2 bugs that are fully deterministic and fully diagnosable from the command line in under a minute, once you know which two fields to compare.
Building or studying robotics?
Arcbotix publishes practical guides on robotics engineering, control systems and real-world builds. Explore more articles on the blog.