> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cerulion.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Backpressure and deadlines

> Choose a queue policy for a fast producer, cap a node's rate, and react when data arrives late or a publish is missed.

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

```rust theme={null}
#[cerulion_node]
#[derive(Default)]
struct Detector {
    #[input(trigger, depth = 4, backpressure = drop_oldest)]
    image: Image,

    #[output]
    detection: PoseStamped,
}
```

| Policy                  | What happens                                                                                                                                         | Reach for it when                                                                          |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `drop_oldest` (default) | When the queue is full, the oldest unconsumed frame is reclaimed for the new one. Latest wins.                                                       | Sensor data where a stale frame is worthless — camera images, lidar sweeps, odometry.      |
| `sample(N)`             | A read is accepted only if its publish timestamp is at least N ms after the last accepted one; anything closer is decimated and counted.             | Deliberately consuming a fast topic at a slower rate — a 100 Hz lidar into a 10 Hz logger. |
| `block`                 | The **producer's** tick is deferred the moment the consumer's queue reaches its declared depth, before anything can overflow. No frame is ever lost. | A stream where every message matters — commands, transactions, state deltas.               |

`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](/cerulion/guides/run-multi-process).
* 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:

```rust theme={null}
#[cerulion_node(throttle_ms = 100)]
```

`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.

| Attribute                              | Watches                    | Fires when                                                              |
| -------------------------------------- | -------------------------- | ----------------------------------------------------------------------- |
| `#[input(expect_within_ms = N)]`       | An input's arrival rate    | N ms elapse with no fresh data on that input.                           |
| `#[output(promise_within_ms = N)]`     | Your own publish rate      | N ms elapse with no publish on that output.                             |
| `#[cerulion_node(tick_within_ms = N)]` | Your tick's execution time | A tick takes longer than N ms. Counter only — there is no event for it. |

```rust theme={null}
#[cerulion_node(period_ms = 20, tick_within_ms = 15)]
#[derive(Default)]
struct Controller {
    #[input(expect_within_ms = 100)]
    odometry: Odometry,

    #[output(promise_within_ms = 25)]
    cmd_vel: Twist,

    odometry_stale: bool,
}
```

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:

```rust theme={null}
#[cerulion_node_impl]
impl Controller {
    fn tick(&mut self) -> Result<(), NodeError> {
        Ok(())
    }

    #[on_event(input = "odometry")]
    fn on_odometry_late(&mut self, event: ExpectWithinEvent) {
        self.odometry_stale = true;
        tracing::warn!(elapsed_ms = event.elapsed_ms, "odometry overdue");
    }

    #[on_event(input = "odometry")]
    fn on_odometry_pressure(&mut self, event: BackpressureEvent) {
        tracing::warn!(policy = ?event.policy, dropped = event.dropped, "odometry backpressure");
    }
}
```

| Event                | Filter   | Meaning                                                       |
| -------------------- | -------- | ------------------------------------------------------------- |
| `BackpressureEvent`  | `input`  | Frames were dropped, decimated, or the producer was deferred. |
| `ExpectWithinEvent`  | `input`  | An input's arrival deadline was missed.                       |
| `PromiseWithinEvent` | `output` | Your own publish deadline was missed.                         |
| `LivelinessEvent`    | `input`  | A publisher on that input appeared or went away.              |

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.

<Warning>
  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.
</Warning>

## 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

<CardGroup cols={2}>
  <Card title="Node macro reference" icon="code" href="/cerulion/reference/node-macro" color="#0080FF">
    Every attribute and its exact validation rules.
  </Card>

  <Card title="Trigger policies" icon="timer" href="/cerulion/reference/trigger-policies" color="#0080FF">
    When a node fires in the first place.
  </Card>
</CardGroup>
