ROS 2 Executors and Callback Groups for Robot Timing

ROS 2 callback groups decide which callbacks may run concurrently, while an executor schedules ready callbacks on available threads. A Multi-Threaded Executor can still behave serially when work shares one mutually exclusive group, and synchronous calls can deadlock when their completion callback cannot run.

Robot timing failures often appear as stale sensor data, late control timers or blocked services. The cause may be callback execution, wait time before execution, a lock, synchronous service use or middleware queues. Thread count alone does not identify the bottleneck.

Use this guide with the ROS 2 real-time control guide and ROS 2 QoS guide. Trace the deployed executor before changing its configuration. If the missed deadline is inside the read-update-write path, move to the ros2_control hardware-interface guide.

How executors and callback groups divide scheduling

Subscriptions, timers, services, clients and waitables become executable callbacks when ready. The executor waits for work, selects eligible callbacks and invokes them according to its implementation and callback-group rules. It does not automatically infer which work is urgent for the robot.

List every callback with expected rate, maximum execution time, shared resources and deadline. Include hidden callbacks used by synchronous clients or futures. An incomplete inventory makes deadlock analysis and response-time claims unreliable.

NVIDIA Jetson Nano board with processor heatsink memory slot and peripheral interfaces
Robot callbacks share finite CPU, memory and I/O resources; this development board is an example platform, not evidence of any executor configuration. Source: Ubahnverleih via Wikimedia Commons. License: CC0 1.0.

A single thread makes blocking visible

A Single-Threaded Executor executes one callback at a time. A long image callback, blocking disk write or service wait delays every other callback in that executor. The simplicity is useful for determinism analysis when the combined worst-case workload fits.

Measure readiness-to-start wait and start-to-finish execution separately. If a timer begins late, the timer callback may be short while another callback owns the thread. Average CPU utilization can remain low when one blocking call creates the delay.

SymptomLikely layerFirst measurementCommon cause
All callbacks pauseSingle executor threadRunning callback durationBlocking I/O
Only one group serializesCallback groupGroup and thread traceDefault group
Service call hangsDependency cycleFuture and group ownershipSelf-deadlock
Fresh messages arrive lateQueue or executorSample age at callbackSlow consumer
Deadline misses under loadScheduling and contentionTail wait timeShared lock or CPU pressure

Multiple threads do not guarantee parallel callbacks

A Multi-Threaded Executor can run eligible callbacks on several threads, but callback-group restrictions still apply. If all entities use the node’s default mutually exclusive group, they cannot execute in parallel with one another even when many executor threads exist.

Inspect group assignment at entity creation and confirm that the group remains associated with the executor. The current ROS 2 executor concepts should be read for the deployed distribution because implementation behavior can evolve.

Use mutually exclusive groups for protected sequences

A mutually exclusive callback group prevents its callbacks from running simultaneously with one another. This can protect non-thread-safe state without a separate lock and can preserve ordering assumptions within the group. It can also cause unrelated long work to delay a critical timer.

Group callbacks by actual concurrency requirement, not simply by node. Put device transactions that must serialize together, while separating independent telemetry or expensive processing when the shared-state design permits. Document why each group is exclusive.

Five-stage ROS 2 executor callback delay diagnosis
Waiting time and execution time require separate evidence. Source: Physical AI Lab.

Reentrant groups require thread-safe code

A reentrant group permits callbacks, including multiple instances of the same callback, to overlap. It can improve concurrency for independent work, but it does not make accessed libraries, buffers or devices thread safe. Race conditions can corrupt state while benchmarks appear faster.

Review shared variables, publishers, clients, device handles and third-party libraries. Use bounded synchronization and avoid holding a lock across I/O. Stress with repeated overlapping requests and a thread sanitizer or equivalent test where practical.

Group choiceConcurrencyUseful forPrimary risk
Mutually exclusiveOne callback in groupOrdered device accessHidden serialization
ReentrantOverlapping callbacksIndependent stateless workData races
Separate exclusive groupsParallel across groupsIndependent serialized devicesCross-group shared lock
Dedicated executorSeparate thread poolCritical path isolationCoordination overhead
Non-ROS workerExplicit handoffBlocking or batch workQueue ownership

Avoid synchronous self-deadlock

A callback that makes a synchronous service request may wait for a response callback that must run in the same mutually exclusive group. The waiting callback holds the group’s eligibility, so the response cannot execute. A similar cycle can occur with futures and actions.

Prefer asynchronous flow inside callbacks, place dependent callbacks in compatible groups or use a carefully designed separate executor. Draw the wait-for graph and test timeouts. Adding threads does not break a group-level exclusion cycle.

Size from execution and wait distributions

For each callback, record invocation rate, execution-time percentiles, maximum observed time and readiness wait. Add CPU affinity, thread identity and lock wait when possible. A thread pool should be sized from concurrency and blocking behavior, not from processor core count alone.

Include serialization, middleware take, memory allocation, logging and cache effects. Run long enough to expose thermal throttling and background jobs. A high throughput result can coexist with unacceptable control-tail latency.

Priority is not automatic

Executor selection alone does not guarantee operating-system thread priority, callback priority or bounded preemption. Critical and best-effort callbacks can share a worker thread or lock. A ready low-importance callback may run before a control timer depending on executor behavior.

Where deadlines matter, isolate critical work, configure scheduling and affinity deliberately, remove blocking operations and measure under worst-case contention. The ROS 2 real-time demo describes supporting practices, not a certification of an arbitrary application.

Separate QoS delay from executor delay

QoS affects compatibility, retention and transport behavior. The executor affects when an already-ready callback runs. Both can increase sample age. Changing Reliable to Best Effort may reduce a network backlog but cannot fix a callback blocked on a mutex.

Carry source timestamps and sequence numbers to the callback. Record receive readiness, callback start and consumption time. This locates the delay before or after middleware delivery and prevents tuning the wrong layer.

Diagnose with trace and controlled load

Trace callback ready, start and finish events, executor thread, group identity and relevant locks. Add one load source at a time: high-rate sensors, service bursts, slow I/O, CPU stress and network loss. Preserve a failing trace before changing the design.

Then move or split one callback group, replace one synchronous dependency or offload one blocking task. Repeat the same workload. Adaptive measurements provide stronger evidence than a complete rewrite that changes several timing layers at once.

Isolate the real-time path from convenience work

Visualization, parameter services, diagnostics, bag recording and model inference can interfere with control if they share executor threads, locks or memory. Use bounded queues and explicit ownership when crossing into a dedicated control thread or executor.

Isolation does not eliminate communication delay. Define the newest valid state, drop policy and watchdog at each boundary. Verify that best-effort work cannot fill a queue consumed by the critical path.

Accept by deadline and data age

Count throughput, but base acceptance on callback wait, execution tail, missed timer periods, sample age at use and recovery after overload. Test the same callback graph after software and hardware changes.

Publish an executor contract with groups, thread pool, scheduling, shared locks and worst-case workload.

  • Inventory every callback and hidden dependency.
  • Trace wait and execution time separately.
  • Map callback groups and shared resources.
  • Inject CPU, I/O, service and sensor load.
  • Accept by deadlines and data freshness.

Frequently asked questions

Does Multi-Threaded Executor run every callback in parallel?

No. Callback-group rules, ready work, thread count and shared locks determine actual concurrency.

Can every callback stay in the default group?

It can, but the default mutually exclusive group may serialize work and defeat the intended parallelism.

Is a Reentrant group always faster?

No. It only permits overlap; contention, races and extra synchronization can reduce performance or correctness.

Will Best Effort QoS fix executor delay?

Not when the delay comes from callback scheduling, locks or blocking code. Measure both layers.

What should be measured first?

For each callback, measure source data age, ready-to-start wait, execution duration, thread and callback group.

Executor Timing Evidence Boundary

Executor timing depends on ROS distribution, RMW, operating system, hardware, callback code and workload. Measure the deployed graph; executor configuration alone is not a real-time guarantee.