ExplorRobo

ROS 2 Quality of Service policies explained

By ExplorRobo Editorial · · 14 min read

Why ROS 2 has QoS at all

ROS 2 tunes communication between nodes with Quality of Service policies. The point is stated directly in the concepts page: with the right set of policies ROS 2 can be as reliable as TCP or as best effort as UDP, with many states in between (QoS settings, Jazzy docs). ROS 1 mostly offered one behaviour, a TCP-style transport with a queue. ROS 2 exposes the DDS transport underneath, which turns a lossy wireless link or a real time deadline into a configuration decision.

The unit of configuration is the endpoint: a QoS profile is set independently for publishers, subscriptions, service servers and service clients. That independence explains both the flexibility and the most common beginner failure. Profiles set independently can be incompatible, and when they are, the publisher reports no error, only silence.

If the framework is new to you, start with What is ROS 2?, and for the interfaces carrying the traffic see ROS 2 topics vs services vs actions.

The policies

The documented base profile carries eight settings (QoS settings, Jazzy docs):

  • History: keep last, storing only N samples, or keep all, bounded only by the middleware’s resource limits.
  • Depth: the queue size, honoured only with keep last history.
  • Reliability: best effort, which tries to deliver and may lose samples on a poor link, or reliable, which retries.
  • Durability: transient local, where the publisher holds samples for late joining subscriptions, or volatile, where nothing is retained.
  • Deadline: the expected maximum interval between two messages on the topic.
  • Lifespan: how long a sample stays valid between publication and reception. Expired samples are dropped silently.
  • Liveliness: automatic, where any one publisher on a node proves the node is alive, or manual by topic, where the publisher asserts liveness through the API.
  • Lease duration: how long a publisher may go without asserting liveness before the system declares it not alive.

Non-duration policies also have a system default that defers to the middleware, and duration policies have a default that leaves the duration unspecified, usually read as infinite. The distinction matters: system default hands the decision to the RMW implementation, and implementations may resolve it differently.

The preset profiles

Choosing eight policies per endpoint would be unusable, so ROS 2 ships named profiles. The rmw header that defines them lists the values (rmw qos_profiles.h, Jazzy):

  • default, used by publishers and subscriptions: keep last with depth 10, reliable, volatile.
  • sensor_data: keep last with depth 5, best effort, volatile.
  • services_default: keep last with depth 10, reliable, volatile.
  • parameters and parameter_events: keep last with depth 1000, reliable, volatile.
  • system_default: middleware defaults for every policy.

The default profile imitates ROS 1 behaviour, so publishers and subscriptions connect out of the box. Sensor data goes the other way: the newest reading promptly is worth more than every reading, so it trades reliability for latency and uses the shallower queue of 5, which is the value behind SensorDataQoS in C++ and qos_profile_sensor_data in Python. Services must keep volatile durability, because the documentation notes that a service server which restarts could otherwise consume an outdated request and be protected from nothing. Parameters follow services with the deeper queue of 1000 so requests survive a client that cannot reach the server.

Two presets sit outside that list. best_available matches the majority of endpoints present at creation time, and the header warns to use it with care, since the middleware does not update policies after the endpoint exists and discovery races can produce non-deterministic behaviour. unknown is the placeholder for an unresolved profile.

Request versus offered: the compatibility model

Compatibility is not equality. It is a request against an offer: the subscription requests the minimum quality it accepts, the publisher offers the maximum it can provide, and a connection is made only when every requested policy is no more stringent than the offered one. One publisher can serve several subscriptions with different requests, and the compatibility of a pair does not depend on other endpoints on the topic.

Two rules are worth memorising. A best effort publisher and a reliable subscription are incompatible, while a reliable publisher and a best effort subscription connect, because the subscriber asks for less than is on offer. A volatile publisher and a transient local subscription do not connect at all, while a transient local publisher and a volatile subscription connect but receive new messages only. A latched topic visible to late subscribers therefore needs transient local on both sides. The same logic covers deadline, liveliness and lease duration, and a connection needs all of them compatible at once, so one bad durability setting blocks a pair that agrees on everything else.

Setting QoS from the command line

The ros2 topic verbs that create an endpoint expose the policies as flags. ros2 topic pub and ros2 topic echo both accept --qos-profile, --qos-depth, --qos-history, --qos-reliability, --qos-durability, --qos-liveliness and --qos-liveliness-lease-duration-seconds, and the individual flags override the selected preset (add_qos_arguments, Jazzy). Valid --qos-profile values are the preset short keys: default, sensor_data, services_default, parameters, parameter_events, system_default, best_available and unknown.

The two verbs do not start from the same preset, which is easy to miss. ros2 topic pub defaults to default, while ros2 topic echo defaults to sensor_data, so the echo tool subscribes with best effort reliability and depth 5 (pub.py, echo.py). The rosbag2 QoS guide uses exactly that override when checking a replayed topic:

ros2 topic pub -r 0.1 --qos-durability transient_local /talker std_msgs/String "data: Hello World"
ros2 topic echo --qos-reliability best_effort /talker std_msgs/String
ros2 topic pub --qos-profile sensor_data --qos-depth 1 -r 10 /camera/image_raw sensor_msgs/msg/Image "..."

One flag removes a class of test ordering pain: -w or --wait-matching-subscriptions on ros2 topic pub waits until the specified number of matching subscriptions is present, defaulting to 1 with --once or --times and 0 otherwise (pub.py, Jazzy).

Setting QoS in code

In Python, rclpy exposes QoSProfile plus policy enums and the presets. The constructor accepts history, depth, reliability, durability, lifespan, deadline, liveliness, liveliness_lease_duration and avoid_ros_namespace_conventions, and enforces two rules: either history or depth must be given, and keep last history without a depth raises InvalidQoSProfileException (rclpy qos.py, Jazzy). The enums are HistoryPolicy, ReliabilityPolicy, DurabilityPolicy and LivelinessPolicy, with the older QoS prefixed names kept as aliases, and presets are available as constants such as qos_profile_sensor_data or through the QoSPresetProfiles enum:

from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy

qos = QoSProfile(
    depth=1,
    reliability=ReliabilityPolicy.BEST_EFFORT,
    durability=DurabilityPolicy.VOLATILE,
)
self.pub = self.create_publisher(Image, '/camera/image_raw', qos)

The module also offers qos_check_compatible(publisher_qos, subscription_qos), which returns a QoSCompatibility value and a reason string, and reports a warning rather than a verdict when either profile still carries a system default or unknown value that has not been resolved.

In C++, rclcpp names the presets as classes and keeps the policies as chainable methods on rclcpp::QoS (rclcpp qos.hpp, Jazzy). The classes are SensorDataQoS, SystemDefaultsQoS, ServicesQoS, ParametersQoS, ParameterEventsQoS, RosoutQoS, ClockQoS and BestAvailableQoS; the methods include keep_last, keep_all, reliable, best_effort, durability_volatile, transient_local, deadline, lifespan and liveliness:

rclcpp::QoS qos(rclcpp::KeepLast(1));
qos.best_effort().durability_volatile();

auto pub = create_publisher<sensor_msgs::msg::Image>("/camera/image_raw", qos);
auto sub = create_subscription<sensor_msgs::msg::Image>(
  "/camera/image_raw", rclcpp::SensorDataQoS(), callback);

A constructor given only a depth bases itself on the default profile, reliability reliable and durability volatile, as the header states. When a profile contains system default values, the middleware resolves them at endpoint creation, and get_actual_qos() returns the settings actually applied, which answers what your endpoint is really using (subscription_base.hpp, Jazzy).

Detecting incompatibility instead of guessing

Silence is the default symptom, so measure before changing anything. ros2 topic info --verbose prints the QoS block of every matched endpoint (Understanding topics, Jazzy docs), covering Reliability, History (Depth), Durability, Lifespan, Deadline, Liveliness and Liveliness lease duration for each publisher and subscription. In the documented example the publisher reports KEEP_LAST (7) and a subscription KEEP_LAST (5); that difference is harmless, because history and depth are not part of the compatibility test.

ROS 2 also reports a mismatch as an event. The concepts page lists the QoS events an endpoint can subscribe to: a publisher can learn it missed its offered deadline, that it lost liveliness, or that it met a subscription whose request it cannot satisfy; a subscription can learn it missed its requested deadline, that a publisher’s liveliness changed, or that a publisher offers something it cannot accept. Matched events fire whenever a connection is established or dropped.

In Python the callbacks are attached at endpoint creation through SubscriptionEventCallbacks and PublisherEventCallbacks, which take keyword arguments named deadline, incompatible_qos, liveliness, matched, incompatible_type, and message_lost for subscriptions only. Both containers take use_default_callbacks, on by default, so a mismatch logs a message even in code that sets none (rclpy event_handler.py, Jazzy). Runnable matched event demos exist for both languages: matched_event_detect.py and matched_event_detect.cpp.

Bags are a special case, since the recorder is another subscriber that can fail to match for the same reasons. Rosbag2 adapts the profile it requests or offers per topic, and where that is not enough, --qos-profile-overrides-path takes a YAML file mapping topic names to policies on both record and play. The schema keys are history, depth, reliability, durability, lifespan, deadline, liveliness, liveliness_lease_duration and avoid_ros_namespace_conventions, and duration policies need sec and nsec (Overriding QoS policies, Jazzy docs). If one topic is missing from a bag while everything else recorded, that file is the standard fix, and the details are in How to use rosbag2 to record and replay ROS 2 bags.

Pitfalls

  • A best effort publisher and a reliable subscription never connect, the most common mismatch, because the sensor data profile is best effort and the default subscriber profile is reliable.
  • A volatile publisher cannot satisfy a subscription requesting transient local, and the result is no communication at all, not a partial stream.
  • system default is not the default profile. It defers to the RMW implementation, which may resolve it differently.
  • History and depth do not affect compatibility, but they decide how much data survives a slow consumer. Depth only matters with keep last history.
  • Lifespan drops samples silently, so a slow consumer sees unexplained gaps.
  • The CLI tools hide the difference: ros2 topic echo subscribes with the sensor data profile while ros2 topic pub publishes with the default profile.

When to change the defaults, and when not to

The default profile suits command and state topics, where a lost message is a problem. Switch to sensor data for high rate streams where freshness beats completeness: cameras, LiDAR, IMUs and joint state feedback. Reach for transient local when a late joiner needs the last known value, such as a map, a robot description or a startup topic. Leave durability volatile for services and parameters, treat best available as a debugging aid rather than a production default, and confirm the result on both ends with ros2 topic info --verbose.

The behaviour above was checked against the Jazzy documentation and the Jazzy branches of rmw, rclpy, rclcpp and ros2cli. Jazzy Jalisco is supported until May 2029, Lyrical Luth is the current LTS with an end of life in May 2031, and Kilted Kaiju reaches the end of its support in December 2026 (Releases).

Sources

Frequently asked questions

Which ROS 2 QoS policies decide whether a publisher and a subscriber can connect?
Reliability, durability, deadline, liveliness and liveliness lease duration. Compatibility follows a request versus offered model: the subscription requests the minimum quality it will accept and the publisher offers the maximum quality it can provide, and a connection is made only when every requested policy is satisfied. History and depth are not part of that test.
What are the default QoS policies for a ROS 2 publisher or subscriber?
Keep last history with a depth of 10, reliable reliability, volatile durability and system default liveliness. Deadline, lifespan and lease duration are left unspecified, which the middleware normally interprets as infinite. The sensor data profile differs: keep last with depth 5, best effort reliability and volatile durability.
Why does a subscription receive nothing from a node that is clearly publishing?
Most often a QoS mismatch rather than a bug in the message. A publisher on the sensor data profile offers best effort, and a subscription created with the default profile requests reliable, which is the one combination that will not connect. A publisher offering volatile durability also cannot satisfy a subscription requesting transient local. Compare both sides with ros2 topic info --verbose and match the profile on the subscriber.
How do I make a topic visible to subscribers that join late?
Both the publisher and the subscriber have to use transient local durability, which makes the publisher keep samples for late joiners. That is the ROS 2 replacement for ROS 1 latching. In the CLI, ros2 topic pub accepts --qos-durability transient_local; in code, rclcpp has transient_local() and rclpy has DurabilityPolicy.TRANSIENT_LOCAL.
Can rosbag2 record a topic whose QoS does not match the recorder?
Rosbag2 adapts the profile it requests for each topic, and where that is not enough you can force one. Both ros2 bag record and ros2 bag play accept --qos-profile-overrides-path pointing at a YAML file that maps topic names to policies such as reliability, durability, history and depth, with sec and nsec values for duration policies.

Related reading