FixTheRobot Explore Robotics / Systems Engineering / Engineering Guides / Module 17

Engineering Guides · Module 17

ROS 2 QoS, intra-process communication and executors

A publisher and a subscription connect only if their QoS is compatible. Latency and CPU then depend on where messages are copied and which threads run the callbacks.

  • Intermediate
  • Module 17: System Performance & Optimization
  • Written for ROS 2 Jazzy where ROS 2 is used
  • Sources checked 2026-11-06

Assumptions: Numbers in the examples are illustrative and the code is executed by the site’s tests. Facts taken from a cited source are marked; other statements are general engineering knowledge and must be confirmed on the datasheet of the part you use.

1. Engineering problem

Three mechanisms shape the performance of a ROS 2 system: QoS decides whether endpoints connect at all and how data is delivered, intra-process communication removes copies between nodes in one process, and executors with callback groups decide which callbacks run in parallel.

2. When is this needed?

  • A subscription receives nothing.
  • Large messages cost too much CPU.
  • A node’s callbacks block each other.

3. Selection and design decisions

From the Jazzy documentation (read 2026-11-06): QoS compatibility follows a request-versus-offered model: a connection is made only if every policy of the subscription’s request is not more stringent than what the publisher offers. Reliability: best-effort to best-effort yes; best-effort publisher to reliable subscription no; reliable publisher to best-effort subscription yes; reliable to reliable yes. Durability: volatile to volatile yes (new messages only); volatile publisher to transient-local subscription no; transient-local publisher to volatile subscription yes (new messages only); transient-local to transient-local yes (new and old messages: a latched topic needs both). Deadline and liveliness policies have similar rules. The default profile has depth 10, reliable, volatile; the sensor-data profile uses best-effort reliability and a smaller queue. Intra-process: publishing and subscribing with std::unique_ptr inside one process can give zero-copy transport; const reference or shared pointer does not. Executors: a single-threaded executor runs callbacks one at a time; the multi-threaded executor uses a pool, and parallelism depends on callback groups: callbacks in a mutually exclusive group never run in parallel, those in a reentrant group may; a node’s default callback group is mutually exclusive, so a multi-threaded executor does not help unless other groups are specified; a synchronous call inside a callback can deadlock if the response callback is in the same exclusive group. Loaned messages enable zero-copy with middleware support and only for plain fixed-size message types.

Overview
TopicRuleEffect
ReliabilityReliable subscription needs a reliable publisherSilent no-connection if violated
DurabilityTransient-local subscription needs a transient-local publisherLate joiners miss latched data otherwise
Sensor data profileBest effort, small queueTimely over complete
Intra-processunique_ptr within one processZero-copy possible
Callback groupsDefault group is mutually exclusiveNo parallelism by default
Loaned messagesPlain fixed-size types, middleware supportZero-copy across processes

4. Advantages and disadvantages

  • Reliable QoS guarantees delivery and can add latency and backlog on lossy links; best effort is timely and drops.
  • Composition into one process saves copies and couples failure.
  • More threads increase throughput and the risk of races and deadlocks.

5. Requirements and constraints

  • Every publisher/subscriber pair is checked for compatibility (use ros2 topic info --verbose).
  • Streams use sensor-data QoS; configuration and latched data use transient-local.
  • Executors and callback groups are chosen deliberately for each node.

6. Architecture

publisher (offered QoS) ──▶ [compatible?] ──▶ subscription (requested QoS)
same process: unique_ptr zero-copy; different processes: serialised copy (or loaned message)

7. Implementation

  1. Inspect both ends with ros2 topic info --verbose.
  2. Check the compatibility (below).
  3. Choose QoS by data type: sensor data, commands, latched maps.
  4. Compose nodes that exchange large messages and use unique_ptr.
  5. Assign callback groups and an executor to avoid blocking and deadlock.
REL = {("best_effort", "best_effort"): True, ("best_effort", "reliable"): False, ("reliable", "best_effort"): True, ("reliable", "reliable"): True}   # (publisher offers, subscription requests)
DUR = {("volatile", "volatile"): True, ("volatile", "transient_local"): False, ("transient_local", "volatile"): True, ("transient_local", "transient_local"): True}
def compatible(pub, sub):
    out = []
    if not REL[(pub["reliability"], sub["reliability"])]:
        out.append("reliability: publisher offers best_effort, subscription requests reliable")
    if not DUR[(pub["durability"], sub["durability"])]:
        out.append("durability: publisher offers volatile, subscription requests transient_local")
    return out or ["compatible"]
sensor = {"reliability": "best_effort", "durability": "volatile"}
default = {"reliability": "reliable", "durability": "volatile"}
latched_map = {"reliability": "reliable", "durability": "transient_local"}
print("sensor publisher -> default subscription:", compatible(sensor, default))
print("default publisher -> sensor subscription:", compatible(default, sensor))
print("map publisher (transient local) -> default subscription:", compatible(latched_map, default))
print("default publisher -> late-joining map subscription:", compatible(default, latched_map))

Printed (checked by the site’s tests):

sensor publisher -> default subscription: ['reliability: publisher offers best_effort, subscription requests reliable']
default publisher -> sensor subscription: ['compatible']
map publisher (transient local) -> default subscription: ['compatible']
default publisher -> late-joining map subscription: ['durability: publisher offers volatile, subscription requests transient_local']

Reading it: a sensor publisher (best effort) does not connect to a default subscription (reliable); a default publisher does connect to a sensor-profile subscription; a transient-local map publisher connects to a default subscription (which gets new messages only); and a default publisher does not connect to a late-joining map subscription that requests transient local. The checker encodes the documented tables.

8. Real examples

  • Point-cloud pipeline: driver, filter and detector composed in one process with unique_ptr passing; QoS sensor data.

9. Failure scenarios

  • A reliable subscription on a best-effort camera driver, so no images arrive.
  • A latched map that late subscribers never receive.
  • A synchronous service call in a mutually exclusive callback, causing deadlock.

10. Troubleshooting

  1. Run ros2 topic info /topic --verbose and compare the profiles.
  2. Check QoS events (incompatible QoS) in the logs.
  3. Check whether callbacks block each other with a single mutually exclusive group.

11. Validation

  • The checker prints the lines shown (executed by the site tests).
  • Every intended connection exists and CPU per message is lower after composition where it was applied.

12. Engineering checklist

13. Further references

  • ROS 2 Jazzy, About Quality of Service settings: docs.ros.org, read 2026-11-06.
  • ROS 2 Jazzy, About executors: docs.ros.org, and Using callback groups: docs.ros.org, read 2026-11-06.
  • ROS 2 Jazzy, Intra-process communication demo: docs.ros.org, read 2026-11-06.
  • ROS 2 Jazzy, Configure zero copy loaned messages: docs.ros.org, read 2026-11-06.

Related on FixTheRobot