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.
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.
| Topic | Rule | Effect |
|---|---|---|
| Reliability | Reliable subscription needs a reliable publisher | Silent no-connection if violated |
| Durability | Transient-local subscription needs a transient-local publisher | Late joiners miss latched data otherwise |
| Sensor data profile | Best effort, small queue | Timely over complete |
| Intra-process | unique_ptr within one process | Zero-copy possible |
| Callback groups | Default group is mutually exclusive | No parallelism by default |
| Loaned messages | Plain fixed-size types, middleware support | Zero-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
- Inspect both ends with ros2 topic info --verbose.
- Check the compatibility (below).
- Choose QoS by data type: sensor data, commands, latched maps.
- Compose nodes that exchange large messages and use unique_ptr.
- 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
- Run ros2 topic info /topic --verbose and compare the profiles.
- Check QoS events (incompatible QoS) in the logs.
- 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.