Back to Blog

ROS2 QoS Settings Explained: Why Your Subscriber Gets Zero Messages

A worked example of the QoS mismatch bug that leaves a subscriber silently starved of data, and how to diagnose it.

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

Durability

History

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.

  1. Confirm the publisher exists and is actually publishing with ros2 topic hz run against it directly.
  2. Run ros2 topic info --verbose and compare reliability and durability on both sides.
  3. Match the subscriber's QoS to the publisher's intent, not the ROS2 default.
  4. Re-check with ros2 topic hz from 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.