Skip to main content
A producer that outruns its consumer is the normal case in robotics, not an error: a camera at 60 Hz feeding a detector that takes 40 ms per frame will always be ahead. What you choose is what the consumer does with the frames it cannot keep up with — and Cerulion treats that as a scheduling decision, so no policy ever copies your data into a side buffer.

Pick an input policy

depth = N is the real queue depth, honored exactly — default 10, maximum 64. Every unit of depth reserves a full-sized shared-memory slot, so a deep queue on an image-class topic reserves a great deal of /dev/shm. If you want more than 64, the consumer is permanently slower than the producer and the answer is a policy, not a bigger queue.

block has real constraints

block is lossless end to end, which is exactly why it constrains the graph:
  • It is only installed when every consumer of the topic declares it.
  • The topic must have an in-graph producer — an external publisher cannot be deferred, and the build says so.
  • Under multi-process execution, the producer and every block consumer land in one process group automatically, because a producer can only be deferred from inside its own process. A hand-written process_groups: that splits one is refused before any worker starts. See Run a graph across processes.
  • A slow consumer holds its producer back. That is the trade you are making: latency for completeness.

Cap the producer instead

When the right answer is “this node should not run this often”, cap the node rather than the edge:
throttle_ms defers the node’s own tick while less than N ms have passed since its last fire. It is mutually exclusive with period_ms — a period already pins the rate — and composes with block: the tick defers if either gate fires.

Set deadlines

Deadlines are QoS, not triggers. They do not change when a node fires; they tell you when reality diverged from the contract, with a counter and a structured warning.
Each is worth a moment of thought before you set it:
  • expect_within_ms is how you notice a producer that stalled without disconnecting. A non-trigger input holds its last value, so a frozen sensor otherwise looks like a healthy one publishing the same reading.
  • promise_within_ms is a promise about your output, which makes it the deadline a downstream team can hold you to.
  • tick_within_ms catches the tick that occasionally takes 40 ms in a 20 ms loop — the one that never shows up in an average.

React in code

Handle any of these in the node itself with #[on_event]. The event’s parameter type selects the event, and the filter names the port:
Handlers run at the tail of a successful tick, in declaration order, and are edge-triggered — one event per regime, not one per occurrence. They do not run at all if tick() returned Err. Every handler needs a filter matching its event’s scope, and there is no node-wide handler and no tick-deadline event: tick_within_ms is a counter. BackpressureEvent fires for all three policies, so branch on event.policy. Its dropped field is the number of messages actually lost this regime — always 0 under block, which is flow control rather than loss.
Handlers dispatch on the node’s own tick. A purely data-triggered node stops ticking when its input goes silent, so it may never run its LivelinessEvent handler for a disconnect. Give a node that must notice silence a period trigger.

Observe from outside

Every policy and deadline also increments a counter, so a diagnostic node or an operator can read the same facts without changing node code: per-input drop, decimation, and defer counts, and per-input and per-output deadline misses. Counters are the observable truth; the log line is a convenience.

Next steps

Node macro reference

Every attribute and its exact validation rules.

Trigger policies

When a node fires in the first place.