Nav2 RateController: Why Running Actions Still Receive Ticks

A 1 Hz Nav2 RateController can tick a RUNNING child on every parent-tree tick. In an AI-assisted standalone Python reconstruction of its status rules, a child received 13 ticks from synthetic time 0 through 1,200 ms; its terminal result and a subsequent status reset determined when it was ticked again. This was not a Nav2 execution or a count of planner requests. The pinned Jazzy source explains the distinction: first visit, a running child and an elapsed period are separate reasons to tick.

Consider a trace that shows repeated child activity while a planner request is in progress. Before changing the frequency, identify what each event measures. Progress handling, a fresh action goal and a completed path are different events. Treating them as equivalent can turn an ordinary asynchronous trace into an apparent rate-control fault.

Separate four events in a navigation trace

The Nav2 behavior-tree walkthrough for Jazzy places RateController hz="1.0" around the ComputePathToPose subtree inside a PipelineSequence. The same tree has a FollowPath subtree, and the walkthrough explains that planning can be revisited while the robot follows a path. Its 1 Hz setting is an example intended to avoid flooding the planning server at the faster tree update rate. It is not a claim that all ticks or all planner work occur exactly once a second.

Event to log What it means What it does not establish
Parent tree tick The navigation tree had a chance to advance. That the planner was asked for a new path.
RateController child tick The decorator called its child, including continued RUNNING work. That a new action goal was sent.
New planner action request A fresh path calculation was actually requested. That a usable path was returned immediately.
Planner result and path update A request completed and a path became available or failed. That the controller consumed the path at that same instant.

Instrument these as separate timestamps, keyed by action goal or request identifier where the application exposes one. A useful trace shows the decorator state, child status, planner request start and completion, and the path version sent to the follower. If the child remains running for longer than the configured period, frequent child ticks are expected by the source logic. If the planner receives repeated new goals during that time, inspect the action node and surrounding tree as well; changing only the hz value may not diagnose the cause.

For a hypothetical warehouse route, suppose the tree continues ticking while a costmap update makes the old path less useful. You would compare how quickly a genuinely new request is sent and a replacement path reaches FollowPath, not count RateController invocations. That warehouse scenario is a proposed measurement, distinct from the standalone state-model experiment below. The global planner and local controller guide gives the larger planning-and-following context; the issue here is narrower: what the decorator actually throttles.

Keeping or resetting status changed the next eligible tick

We ran an AI-assisted standalone Python state model on October 4, 2026, using Python 3.12.14. It reconstructs selected branches of Jazzy’s RateController at commit 645abd9. The clock, child responses and parent resets are synthetic inputs; no Nav2 plugin, ROS process or planner was executed.

The modeled parent ticks every 100 ms. Its child returns RUNNING until the first call at 1,200 ms, then returns one terminal result. Every later child call returns RUNNING. Each callback consumes zero modeled time. With a 1,000 ms period, all four cases below produced 13 child calls through 1,200 ms. These calls represent progress opportunities, not 13 requests for paths.

What happens at or after 1,200 ms First subsequent child tick Reason in the model
Child succeeds; decorator status retained 2,200 ms SUCCESS restarts the period at 1,200 ms.
Child fails; decorator status retained 1,300 ms FAILURE leaves the clock anchored at 0; its period has already elapsed.
Child succeeds; parent resets decorator to IDLE at 1,300 ms 1,300 ms A fresh iteration takes the first-visit branch.
Deliberately changed control: failure also resets the timer 2,200 ms This extra reset changes the outcome; it is not upstream behavior.

“Retained” matters. In the examined BehaviorTree.CPP 4.6.2 status definition, success and failure both count as active. Its decorator wrapper resets a terminal child’s status to IDLE; that is distinct from the surrounding tree resetting the decorator itself. We modeled those selected rules. This examined library version is not a claim about the dependency installed on your robot.

Run the following with Python; it needs no additional packages. Each output line prints the 13-call count and the subsequent tick time shown above. The last case intentionally changes one rule to test the consequence, not to recommend a patch.

def trace(terminal, reset_at=None, reset_failure=False):
    # Synthetic milliseconds; standalone model, not a Nav2 plugin execution.
    parent = child = "IDLE"
    start, first, finished = 0, False, False
    rows = []
    for t in range(0, 2301, 100):
        if t == reset_at:
            parent = child = "IDLE"  # explicit surrounding-tree intervention
        if parent in ("IDLE", "SKIPPED"):
            start, first = t, True
        parent = "RUNNING"
        called = first or child == "RUNNING" or t - start >= 1000
        returned = "not ticked"
        if called:
            first = False
            returned = terminal if t >= 1200 and not finished else "RUNNING"
            finished |= returned in ("SUCCESS", "FAILURE")
            child = parent = returned
            if returned == "SUCCESS" or (reset_failure and returned == "FAILURE"):
                start = t
            if child in ("SUCCESS", "FAILURE"):
                child = "IDLE"  # selected BT.CPP decorator-wrapper behavior
        rows.append((t, called, returned, start, parent, child))
    return rows

for name, args in [
    ("success retained", ("SUCCESS", None, False)),
    ("failure retained", ("FAILURE", None, False)),
    ("success then IDLE", ("SUCCESS", 1300, False)),
    ("changed failure reset", ("FAILURE", None, True)),
]:
    rows = trace(*args)
    before = sum(t <= 1200 and called for t, called, *_ in rows)
    next_tick = next(t for t, called, *_ in rows if t > 1200 and called)
    print(name, "calls through 1200 ms:", before, "next child tick:", next_tick)

For a real trace, add the decorator’s entry status and reset events before diagnosing excessive replanning. Then count actual action goals separately. This experiment distinguishes scheduling branches; it does not measure a real planner’s workload, clock precision or response latency. Skipped children, callback hooks and custom action-node behavior were outside the model.

Read the source’s timer rule before tuning hz

The pinned Jazzy code calls executeTick() under the first-visit, running-child or elapsed-period condition. SUCCESS starts the clock again; RUNNING does not suppress the next child tick. The surrounding tree’s status handling still affects later scheduling. The label “1 Hz planner” therefore needs a defined action lifecycle to be interpretable.

Choose a replanning interval from two measurements: how quickly the world or goal can invalidate a path, and how much planner work the system can sustain. A slower interval may save computation but leave a stale path longer. A faster interval may improve response to changes while creating unnecessary planner requests or contention. Change one parameter at a time under the same route and obstacle conditions. Track request rate, completion latency, path age at the follower and missed or late responses. The right value belongs to the robot and environment, not to the documentation example.

Version matters here because this article’s precise RUNNING behavior comes from the cited Jazzy source revision. Recheck the plugin source and tree XML for the deployed Nav2 distribution before carrying over this explanation. The walkthrough describes a representative tree; a custom tree may decorate a different child or reset statuses differently. No Nav2 system was executed for this article, and the reasoning above does not establish safe navigation or a universal replanning frequency.