> Source: https://nexusatlas.dev/docs/concepts/scheduling — Nexus Atlas developer documentation (Concepts). Converted from the HTML page; the page is canonical.

# Multi-link scheduling

The scheduler is where Atlas earns its keep: continuous measurement of every link, and a per-packet placement decision that shifts traffic away from trouble *gradually* — not on a binary up/down trigger that flaps. This page covers how links are measured, the eight strategies you can choose from, and the knobs that make asymmetric bearers behave.

## Probes: how a link earns trust

Every (peer, link) pair is probed on its own clock — `[link_monitor] probe_interval_ms`, default 250 ms. Each probe updates an RTT EWMA, a windowed jitter figure and a windowed loss rate. Two principles govern the bookkeeping:

- **Aliveness requires evidence.** A link is never considered alive before its first completed round trip, and is declared dead after `dead_probe_count` (default 5) consecutive misses.

- **A late probe is not proof of life.** Past `unusable_rtt_ms` (default 10 000; per-link override available) a probe that does return stops counting as evidence the link works — so a link buried under its own transmit backlog reads as *down*, not as "up at 40 s RTT with 0% probe loss". The measured RTT still reaches your dashboards.

Per-link overrides matter on mixed bonds: `[[link]] probe_interval_ms` lets a cellular link answer every 250 ms while a geostationary VSAT (~600 ms RTT) is probed every 1000 ms, each with timeouts scaled to its own cadence. And because sparse control probes underestimate loss on full-size packets, `data_loss_probe_every_n` (default 10) piggybacks loss accounting on every Nth real data fragment — the loss figure the scheduler acts on is measured on traffic shaped like yours.

## The eight strategies

Set `[scheduler] strategy` to one of:

| Strategy | Placement rule | Reach for it when |
|----------|----------------|-------------------|
| lowest_delivery_time | Default. Per fragment, minimise size/capacity + RTT — capacity- and size-aware. | General bonding; links of unequal speed. |
| weighted_round_robin | Spread packets proportionally to [[link]] weight. | Deliberate load ratios across similar links. |
| broadcast | Every fragment on every alive link; the receiver's anti-replay window deduplicates. | N× bandwidth is cheaper than any loss; survives N−1 failures. |
| priority | Strict failover through priority_order, a list of link names. | A cheap primary and an expensive standby (e.g. satellite backup). |
| shortest_path | SPF on hop counts; local link choice falls back to lowest_delivery_time. | Multi-hop meshes where route stability beats RTT-chasing. |
| adaptive | 1→2→3-link redundancy driven by cost ratios between the best links. | Redundancy that scales with how bad things are. |
| loss_adaptive | Same escalation, driven purely by measured loss (enter at 20% / 50%). | Loss-dominated bearers. |
| latency_adaptive | Same escalation, driven by best-link RTT (enter at 40 ms / 100 ms). | Latency-sensitive traffic where rising RTT is the early warning. |

The three adaptive strategies share an anti-flap discipline: **escalation is instant, de-escalation waits** (`adaptive_dwell_ms`, default 5000) and the enter/exit thresholds form a hysteresis gap, so a link hovering at a threshold cannot make the bond oscillate. For the non-adaptive strategies, `redundancy_factor = N` tops up any placement to the best N links at once.

```
[scheduler]
strategy = "latency_adaptive"
latency_mirror_ms = 40.0        # ≥ this best-link RTT: send on 2 links
latency_triplicate_ms = 100.0   # ≥ this: send on 3
adaptive_dwell_ms = 5000        # de-escalation dwell; escalation is immediate
```

## Gradual by construction

Link scores update on every probe: loss inflates a link's cost geometrically (`1/(1−loss)`), jitter inflates it further, and cost-based strategies shift traffic as the numbers move — long before anything is "down". The only binary events are the ones that should be binary: a dead link leaving the candidate set, and the adaptive strategies stepping their redundancy level. By the time a failing link is formally dead, it was already carrying almost nothing.

## Predictive failover

`[link_monitor] prediction_source` — `none` (default), `rssi`, `position`, or `both` — lets the scheduler act on where a link is *heading*:

- **rssi:** the signal-strength trend from [radio telemetry](https://nexusatlas.dev/docs/concepts/telemetry) is projected `rssi_lookahead_secs` ahead against `rssi_floor_dbm`; a link fading toward the floor has its perceived loss inflated (capped at 75%) so traffic drains off it before it dies.

- **position:** peers advertise position and velocity; the projected distance is scored against each radio's range envelope (`[[link]] max_range_m`, warning ramp from `range_warn_m`) and stretched links get expensive — locally and in mesh route costs.

Prediction only ever biases scheduling. It never declares a link dead — probes remain the sole authority — and dashboards keep showing measured values, never predicted ones.

## Metered links

A link with a data budget declares it: `quota = "20 GB"` with `quota_period` (`monthly`, `weekly`, `daily`, `rolling-30d`), an anchor day, and a warning threshold (`quota_warn_pct`, default 80). Accounting is honest — wire bytes, including framing, probes, handshakes and redundancy copies — and persists across restarts. An over-budget link drops out of the scheduler's candidate set, but **a quota never takes down the last usable path**: when the expensive link is the only link, it carries the traffic and the journal records the crossing.

## Mirrors, stripes and aggregation

In the [transport-class path](https://nexusatlas.dev/docs/concepts/qos), each class's multipath policy compiles to a typed send plan: **Mirror** (a copy on every listed link — redundancy) or **Stripe** (each packet on exactly one link, weighted by effective goodput *capacity × (1 − loss)* — aggregation). Striping is what makes two 20 Mbps bearers carry materially more than one: measured 36.5 Mbps across two such links under 60 Mbps offered load, 91% of the theoretical sum, versus 19.2 Mbps when the same traffic was mirrored. Links that miss a class's latency budget are excluded from its stripe set before weights are computed, and an emptied set falls back to the single best link rather than dropping traffic.

## A tuning checklist for asymmetric bonds

- Declare honest capacities per link (`capacity = "64 kbps"` … `"1.5 Gbps"`) — the default assumption is 80 Mbps, and cost functions divide by it.

- Slow the probe cadence on high-RTT bearers (`probe_interval_ms` per link) and raise their `unusable_rtt_ms`.

- Budget metered links with `quota` so the scheduler, not an invoice, discovers the ceiling.

- Give narrowband radios a slower mesh advertisement cadence — see [route flooding on narrow bearers](https://nexusatlas.dev/docs/concepts/mesh#narrow-bearers).

- Prefer an adaptive strategy when link quality varies faster than a human can retune; prefer `priority` when the economics, not the physics, should decide.
