System designExplainer

ROS 2 QoS at runtime: why a parameter change needs a new publisher

Setting a QoS parameter in ROS 2 stores a value but changes nothing: QoS is fixed at publisher creation, so you destroy and recreate the publisher.

LumaSpec Vision10 min read Share on LinkedIn

On this page 11 sections
  1. 01QoS is a property of the publisher, not of a parameter
  2. 02The request-offer rule that decides compatibility
  3. 03Recreate the publisher: the sequence and the code
  4. 04The silent failure to design around
  5. 05Bandwidth: when BestEffort is the right call
  6. 06What the wrapper can apply live
  7. 07QoS overrides: parameters that apply once, at creation
  8. 08The hardware on the stream
  9. 09Checks before you rebuild
  10. 10When recreating is the wrong tool
  11. 11Questions

You set the QoS parameters on a running ROS 2 camera node, the call returns success, and the topic keeps flowing exactly as before. Nothing is broken: the value is stored, the stream is untouched. Quality of Service in ROS 2 is not a value the node re-reads each cycle. It is written into the transport endpoint when the publisher is created, and only a rebuilt endpoint carries a new profile. This guide explains what a QoS parameter set actually does, how the request-offer rule decides whether subscribers keep receiving data after a change, the destroy-and-recreate sequence that applies a new profile live, and the bandwidth arithmetic that decides when BestEffort is the right profile for a depth or point-cloud stream.

QoS is a property of the publisher, not of a parameter

In rclcpp, you hand an rclcpp::QoS profile to create_publisher() and create_subscription(). The DDS writer or reader is created with that profile at that instant, and there is no documented call that mutates reliability or durability on a live endpoint. A parameter set cannot reach it: the parameter system and the transport endpoint never meet unless your code makes them meet.

The parameter side works like this. A node declares its parameters at start-up with declare_parameter, and it can register a callback with add_on_set_parameters_callback. When you set a parameter, rclcpp checks the name and type, passes the change to the callback, and returns an rcl_interfaces::msg::SetParametersResult. Success there means the callback accepted the value — normally, that it stored it. It does not mean the node applied anything on the wire.

That is exactly what happens when a camera wrapper declares parameters named video.qos_reliability and video.qos_history: your mid-run set is accepted, the callback stores two values for the next endpoint it builds, and the current publisher keeps its original profile. The stream is unchanged because nothing on the transport layer was touched.

The request-offer rule that decides compatibility

Every publisher offers a QoS profile; every subscription requests one. DDS matches a writer to a reader only when every checked policy on the request side is satisfied by the offer. For sensor streams two policies are checked: reliability and durability. History and its depth are not part of the compatibility check — a KeepLast(10) writer matches a KeepLast(1) reader.

Publisher offers Subscription requests Result
Reliable Reliable Match
Reliable BestEffort Match
BestEffort BestEffort Match
BestEffort Reliable No match — the subscription receives nothing
TransientLocal Volatile Match
Volatile TransientLocal No match — the subscription receives nothing

DDS reports an offered-incompatible-QoS status to the writer when this happens, but most nodes never listen for that event. The usual symptom is not an error message. It is a topic that quietly stops.

Recreate the publisher: the sequence and the code

To apply a new profile while the node runs, rebuild the endpoint. The sequence:

  1. In the parameter callback, validate and store the new values, and set a dirty flag instead of touching the transport.
  2. Between captures, destroy the current publisher by resetting its shared pointer; the DDS writer is destroyed with it.
  3. Build the replacement profile from the stored values: rclcpp::QoS(rclcpp::KeepLast(depth)), then .reliable() or .best_effort(), and .transient_local() or .durability_volatile().
  4. Create the new publisher on the same topic name with create_publisher, so DDS discovery re-matches the existing subscriptions.
  5. Wait for discovery to re-match, then verify the offer on the wire (checks below).
// Member state: illustrative names; the rclcpp API names are documented.
rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr img_pub_;
bool qos_dirty_ = false;      // set in the parameter callback
size_t history_depth_ = 10;   // stored value of the history parameter
bool reliable_ = true;        // stored value of the reliability parameter

rclcpp::QoS make_qos() {
  rclcpp::QoS qos(rclcpp::KeepLast(history_depth_));
  if (reliable_) { qos.reliable(); } else { qos.best_effort(); }
  return qos;
}

void rebuild_publisher() {    // called when qos_dirty_ is set
  img_pub_.reset();           // destroy: the old writer is unadvertised
  img_pub_ = create_publisher<sensor_msgs::msg::Image>(topic_name_, make_qos());
  qos_dirty_ = false;
}

sensor_msgs::msg::Image, rclcpp::QoS, rclcpp::KeepLast and create_publisher are documented rclcpp API. The member names (img_pub_, qos_dirty_, history_depth_, reliable_) belong to this example.

Plan for the seam: with the default Volatile durability, a subscriber that re-matches after the swap receives nothing published before it matched, so expect to lose one or two frames at the boundary. Pause consumers that count messages or compare consecutive frames.

The silent failure to design around

Subscribers that did not explicitly request BestEffort request Reliable, because Reliable is the ROS 2 default. Flip the offer and they starve, with nothing in the log. You control only your side of the match: a node owned by another team that requests Reliable cannot be fixed from your parameter set.

A depth image that stays black in rviz2 is this same rule seen from the other side — the display's request and the driver's offer disagree. The depth image black in rviz2 fix walks through matching the topic QoS on that end.

Bandwidth: when BestEffort is the right call

BestEffort is not a lesser profile. For high-rate sensor data it is the correct one, and the arithmetic shows why:

bitrate (Mb/s) = pixels × bytes per pixel × fps × 8 / 1,000,000

Worked example, a VGA-class depth stream at 30 fps:

  • Pixels: 640 × 480 = 307,200.
  • Depth image, float32, 4 bytes per pixel: 307,200 × 4 = 1,228,800 bytes ≈ 1.23 MB per frame; at 30 fps, 1,228,800 × 30 × 8 = 294,912,000 b/s ≈ 295 Mb/s.
  • XYZ point cloud, three float32 fields, 12 bytes per pixel: 307,200 × 12 = 3,686,400 bytes ≈ 3.69 MB per frame → 110.6 MB/s ≈ 885 Mb/s.
  • XYZ plus packed RGB, 16 bytes per pixel: 4,915,200 bytes ≈ 4.92 MB per frame → 147.5 MB/s ≈ 1,180 Mb/s.

A 1 GbE link moves about 1,000 Mb/s of useful load. Depth alone fits with headroom at 295 Mb/s. Depth plus an XYZRGB cloud needs 1,475 Mb/s and does not fit. The decision: publish the point cloud BestEffort with KeepLast(1) so late frames drop instead of queuing, or keep the cloud off the wire and rebuild it from the depth image on the receiving host.

History depth matters as much as reliability. A Reliable publisher with KeepLast(10) at 30 Hz holds up to 10/30 ≈ 333 ms of stale frames once the link saturates, and the subscriber then works through a third of a second of old data. BestEffort with a shallow history discards late frames, so the consumer always sees the newest complete image.

Stream Reliability Durability History
Depth image, point cloud BestEffort Volatile KeepLast(5) — rclcpp::SensorDataQoS
Control, status, low-rate poses Reliable Volatile KeepLast(10) — the default profile
Values a late joiner must see Reliable TransientLocal KeepLast(10)

What the wrapper can apply live

Content parameters are a different story: the callback forwards them to the camera driver and they affect the next capture. In the naming pattern these wrappers use, video.auto_exposure turns automatic exposure on or off, video.exposure and video.gain take effect while auto exposure is off, depth.confidence is the threshold that filters weak depth values out of the depth map and point cloud, depth.max_depth cuts the range, and general.mat_resize_factor scales the output images.

Setting these mid-run is safe and immediate. Three details decide whether the value you set is the value you get: a 3D camera often needs two captures after a parameter change before the new setting reaches the image (why that happens); the depth unit and precision settings change what a number like depth.max_depth compares against (what they change); and exposure or gain must sit inside the camera's allowed range, so read the range before you set it (query the option range first).

QoS overrides: parameters that apply once, at creation

ROS 2 has a parameter-driven way to set QoS without code changes: overrides. A node accepts parameters named qos_overrides./<topic>.<policy> — for a topic /depth_image, qos_overrides./depth_image.reliability takes reliable or best_effort, qos_overrides./depth_image.durability takes volatile or transient_local, history takes keep_last or keep_all, and depth takes an integer. Set them in the node's YAML parameters file or on the command line with -p.

The catch is timing: rclcpp reads overrides when it constructs the publisher or subscription. Changing an override while the node runs updates the stored value and leaves the live endpoint alone — the same rule that governs the wrapper's own QoS parameters. Use overrides to choose the right profile per deployment at start-up. Use the recreate sequence to change it while running.

The hardware on the stream

Two parts carry this whole discussion: a 3D camera that publishes the depth and point-cloud topics, and the machine that runs your ROS 2 node. The 3D sensors category holds the camera side — laser profilers and structured-light depth cameras — and the camera finder filters it by type.

3D sensorsHikrobot MV-DPS200P-02 Projected structured-light 3D cameraMV-DPS200P-02
Type
Stereo / structured light
$8,829.00 · quotedView and quote

The node and its subscribers run on a vision controller, a fanless industrial computer with the LAN ports for the cameras; if you prefer a panel- or mini-PC form factor for the operator side, industrial panel PCs and industrial mini PCs cover those too. On the controller side, the MV-VC5525-256G70-NN below covers the common case with 5+ LAN ports, PoE camera ports, 8 built-in light-output channels, 32 GB of memory and an Intel Core i5.

Vision controllersHikrobot MV-VC5525-256G70-NN VC5000 Vision Controller, 12th-I5, 32GB+256G SSD, Supported PCIEMV-VC5525-256G70-NN
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
PoE
PoE camera ports
Light outputs
8 built-in channels
$4,479.00 · quotedView and quoteBuild a system with it

Checks before you rebuild

A short list before you flip anything on a live stream:

  • Record the current offer: ros2 topic info /camera/depth/image --verbose prints Reliability and Durability for every publisher and subscription on the topic.
  • Inventory the subscribers: every one that requests Reliable must keep a Reliable offer, or it starves.
  • Recompute the load with the formula above when you add a stream, and check it against the link's useful capacity, about 1,000 Mb/s on GbE.
  • Plan for the seam: with Volatile durability, expect one or two lost frames while discovery re-matches.
  • Verify flow after the swap: ros2 topic hz /camera/depth/image should return your configured rate, 30.0 in the example above, within a few percent.
  • Separate stored from applied: ros2 param get on the node for video.qos_reliability shows what the callback holds, which is not proof of what the endpoint offers.

When recreating is the wrong tool

Three cases where rebuilding the endpoint is the wrong answer:

  • The profile only needs to differ between deployments: set qos_overrides in the YAML parameters file and change nothing in code.
  • A subscriber you do not own requests Reliable: keep the offer Reliable and shrink the history depth instead of switching to BestEffort, because the flip stops them.
  • You cannot tolerate the discovery gap: start the replacement publisher on a temporary topic name, move the subscribers over, then retire the old endpoint.

If the topic never flowed in the first place, that is start-up configuration, not a runtime change: renamed or unsupported launch parameters are covered in the launch-parameter fix, and namespace and topic naming for multi-camera setups prevents the crossed-wire cases.

To size the camera and controller pair for a multi-stream depth rig, start from the system configurator or the systems topic page.

Questions

Can ros2 param set change QoS on a running node?

It changes the stored value, not the stream. rclcpp validates the set, hands it to the parameter callback and returns success when the callback accepts it. Unless that callback destroys and recreates the publisher with a new rclcpp::QoS profile, the DDS endpoint keeps the profile it was created with. Check the actual offer afterwards with ros2 topic info, passing the topic name, plus --verbose.

Why does a topic go quiet after the publisher switches to BestEffort?

A subscription that requests Reliable never matches a writer offering BestEffort. The ROS 2 default profile requests Reliable, so any subscriber that did not explicitly ask for BestEffort is at risk. Keep the publisher Reliable, or set every consumer to BestEffort as well, then confirm matching with ros2 topic info --verbose.

Which QoS profile should a depth or point-cloud topic use?

Start from rclcpp::SensorDataQoS: BestEffort reliability, Volatile durability, KeepLast(5) history. It fits how consumers use sensor streams — newest frame wins, stale frames are disposable. Use the default profile (Reliable, Volatile, KeepLast(10)) for low-rate control and status topics, and TransientLocal only when a late-joining node must receive the last published value.

How long does re-matching take after a publisher is recreated?

Matching rides on discovery traffic, not a fixed timer: on a quiet LAN it typically completes in a fraction of a second, but treat that as an estimate, since it grows with network load. The first frame arrives only after matching completes, so consumers should tolerate one missed period. Measure it with ros2 topic hz on the topic.

Do QoS overrides take effect if I change them while the node runs?

No. rclcpp reads qos_overrides parameters when it constructs the publisher or subscription, so a later change updates the stored parameter value only and the live endpoint keeps its original profile. Use overrides to pick the profile at start-up without code changes, and the destroy-and-recreate sequence to change it while the node runs.