Skip to contentCerulion
Engineering notes
guides

ROS 2 QoS: a field guide

Match publisher and subscriber policies, inspect the active profiles, and make communication failures visible.

Publisher offersBest effortSubscriber requestsReliable
Discovered. Not connected. Reliability policies must be compatible.

The topic exists. The publisher is running. Your subscription receives nothing. Start with the actual endpoint QoS profiles:

ros2 topic info /camera/image --verbose

Two endpoints can be discovered without being compatible. A topic name alone does not prove that messages can pass between them.

The requested-versus-offered rule

Publishers offer policies; subscribers request them. Every policy that participates in compatibility must match the requested-versus-offered rule. Other settings, such as history depth, govern local buffering instead.

The examples below use ROS 2 Jazzy's policy names and APIs. The ROS 2 QoS reference contains the full compatibility tables.

The seven QoS policies

1. Reliability

BEST_EFFORT allows sample loss. RELIABLE adds delivery recovery. A best-effort publisher cannot meet a reliable subscription's request; the reverse pairing is compatible.

Choose between loss and recovery for the actual data stream. Reliability does not guarantee that a callback completes, an actuator executes a command, or an application processes every sample under every resource limit.

2. Durability

VOLATILE serves new samples. TRANSIENT_LOCAL retains publisher history for late subscribers. Receiving retained history requires compatible transient-local settings on both sides, and the relevant samples must still exist.

A volatile publisher cannot satisfy a transient-local subscriber. A transient-local publisher can serve a volatile subscriber, but that pairing receives new samples rather than retained history.

3. History

KEEP_LAST retains a bounded number of samples. KEEP_ALL retains samples subject to the middleware's resource limits; it does not mean unlimited memory or guaranteed application processing.

A slow reader makes buffering and resource limits visible. Monitor queueing, write duration, and loss instead of assuming a history policy removes overload.

4. Depth

Depth sets the sample count for KEEP_LAST. A small depth favors recent data but can discard older samples before processing. A larger depth permits more queued work and can increase the age of a sample when the callback reaches it.

Select depth from the stream's meaning and measured consumer behavior. Logging and control loops need explicit decisions about loss and staleness; a single queue size is not correct for every robot.

5. Deadline

Deadline describes the expected maximum interval between samples. The offered publisher deadline must be no longer than the subscriber's requested deadline. A publisher offering 100 ms is incompatible with a subscriber requiring 50 ms.

Deadline-missed events report a timing violation; they do not automatically recover data or stop a robot. Register a callback when the application needs to act on that event. Deadline compatibility and deadline-missed notifications are separate concerns.

6. Lifespan

Lifespan limits sample age. Expired samples can be discarded before reception. Combining retained history with a short lifespan can leave a late subscriber with no valid historical sample to receive.

Check timestamps as part of application behavior too. Transport expiry does not establish how fresh a sample is when downstream work finishes.

7. Liveliness

ROS 2 exposes automatic and manual-by-topic liveliness. With manual-by-topic, publishing or explicitly asserting liveliness marks the topic as alive; the lease duration limits the time between those signals.

A subscriber requesting manual-by-topic liveliness cannot be satisfied by an automatic publisher. The offered lease duration must also satisfy the requested duration. The RMW header marks the older manual-by-node option as deprecated and directs applications to manual-by-topic. ROS 2 RMW policy definitions.

Compatibility at a glance

PolicyPublisher offersSubscriber requestsResult
ReliabilityBest effortReliableIncompatible
ReliabilityReliableBest effortCompatible
DurabilityVolatileTransient localIncompatible
DurabilityTransient localVolatileCompatible; new samples
Deadline100 ms50 msIncompatible
Deadline50 ms100 msCompatible
LivelinessAutomaticManual by topicIncompatible
LivelinessManual by topicAutomaticCompatible if lease durations are also compatible

Matching one row is not enough: check every compatibility policy on the pair. History and depth need not be identical across publisher and subscriber. The middleware policy definitions explain the requested/offered model alongside resource limits.

Make incompatibility visible

Current ROS 2 client libraries can install default incompatibility warnings when the middleware supports the event. A QoS mismatch is therefore not guaranteed to be silent. For application-specific handling, register event callbacks explicitly. rclpy event handling.

With an existing rclcpp node, configure subscription options before creating the subscription:

rclcpp::SubscriptionOptions options;
auto logger = node->get_logger();
 
options.event_callbacks.incompatible_qos_callback =
  [logger](rclcpp::QOSRequestedIncompatibleQoSInfo & event) {
    RCLCPP_WARN(logger, "Incompatible QoS policy: %d",
      event.last_policy_kind);
  };
 
options.event_callbacks.deadline_callback =
  [logger](rclcpp::QOSDeadlineRequestedInfo & event) {
    RCLCPP_WARN(logger, "Missed deadlines: %d", event.total_count);
  };
 
auto qos = rclcpp::QoS(10);
qos.deadline(rclcpp::Duration::from_seconds(0.1));
 
auto subscription = node->create_subscription<std_msgs::msg::String>(
  "/status", qos,
  [](std_msgs::msg::String::ConstSharedPtr message) {
    // Process the received status.
    (void)message;
  },
  options);

The publisher must offer a compatible deadline. These callbacks belong to SubscriptionOptions::event_callbacks; the event types and callback fields are defined in event_handler.hpp.

The Python equivalent uses SubscriptionEventCallbacks:

from rclpy.event_handler import SubscriptionEventCallbacks
from rclpy.qos import QoSProfile
from std_msgs.msg import String
 
events = SubscriptionEventCallbacks(
    incompatible_qos=lambda event: node.get_logger().warning(
        f"Incompatible QoS policy: {event.last_policy_kind}"
    )
)
subscription = node.create_subscription(
    String, "/status", lambda message: None,
    QoSProfile(depth=10), event_callbacks=events
)

A debugging sequence

1. Inspect the endpoints

ros2 topic info /camera/image --verbose

Check topic type, publisher count, subscription count, and each endpoint's QoS. A missing endpoint sends you toward names, domains, discovery, and node startup. A present but incompatible pair sends you toward the policy settings. The command displays endpoint profiles, not a live counter of deadline-missed events. Topic info implementation.

2. Set the diagnostic subscriber's policies explicitly

For a best-effort, volatile camera publisher:

ros2 topic echo /camera/image \
  --qos-reliability best_effort \
  --qos-durability volatile \
  --once --no-arr

For retained state from a transient-local publisher:

ros2 topic echo /map \
  --qos-reliability reliable \
  --qos-durability transient_local \
  --once --no-arr

Jazzy's echo can adapt its default reliability and durability to discovered publishers. Set the flags explicitly when reproducing a particular pairing; do not assume the diagnostic subscriber has your node's QoS. Topic echo implementation.

3. Separate receipt from processing

ros2 topic hz /camera/image
ros2 topic bw /camera/image

These commands measure what their own subscriptions receive. They are not direct measurements of your application's callback or of the publisher's internal timing. If the CLI receives data while the application does not, compare profiles, callback groups, executor scheduling, and the application's own logs. Topic hz implementation, topic bw implementation.

Common profiles in Jazzy

ProfileReliabilityDurabilityHistoryDepth
Default publisher/subscriptionReliableVolatileKeep last10
Sensor dataBest effortVolatileKeep last5
ServicesReliableVolatileKeep last10
ParametersReliableVolatileKeep last1000
Parameter eventsReliableVolatileKeep last1000
ClockQoSBest effortVolatileKeep last1
SystemDefaultsQoSMiddleware defaultMiddleware defaultMiddleware defaultMiddleware default

SystemDefaultsQoS is not another name for the default reliable/depth-10 profile. It delegates those choices to the middleware. The definitions live in the RMW profiles and rclcpp QoS classes.

Shared memory has its own requirements

A compatible QoS pair does not prove that a zero-copy path is active. Check the specific implementation and message type:

  • Cyclone DDS with its documented iceoryx integration: shared-memory exchange has build, configuration, fixed-size type, and QoS requirements. That integration documents reliable, volatile, keep-last settings; do not replace them with a generic “best effort is faster” rule. Cyclone DDS shared memory.
  • Fast DDS: shared-memory transport and data-sharing delivery are different paths. Application-to-application zero-copy additionally requires loans and supported plain types. Fast DDS zero-copy requirements.
  • Message definitions: sensor_msgs/msg/Image contains a variable-length data sequence. Being an image does not make it a fixed-size type. Image message definition.

Subscriber capacities and sample pools belong to the actual transport configuration. There is no single subscriber limit that describes every shared-memory implementation.

Inspect the profiles, make mismatches visible, and measure the path that is actually running. For what happens between the endpoints, continue with the DDS walkthrough.