Bonding & scheduling

Measured links. Per-packet decisions.

Every link in the bond is an isolated socket with its own queue, its own watchdog and its own probe clock. The scheduler never trusts configuration optimism — it trusts the last few hundred milliseconds of measurements. This page is the mechanics, with the demos inline.

Field-proven core8 strategies · per packet
Sockets

One UDP socket per link, pinned to its device

Each [[link]] binds with SO_BINDTODEVICE and sends through its own worker. The expo configuration bonds six at once — two serial radios, Bluetooth-PPP, Wi-Fi ad-hoc, 802.11s mesh and Ethernet — in one daemon.

Queues

Bounded, drop-on-full, non-blocking

32 packets per link with a 200 ms send timeout: a saturated 64 kbps serial link sheds its own load instead of back-pressuring the TUN reader and stalling the fast links behind it.

Watchdog

Sockets that heal

Three consecutive send errors rebuild the socket and re-pin it to the device. An unplugged, re-enumerated USB radio — or a booth hub power-cycled mid-demo — rejoins the bond without a daemon restart.

Capacity

Declared honestly, used mathematically

Each link declares nominal capacity in human units ("64 kbps", "100 Mbps", "1.5 Gbps"). Delivery-time math keeps bulky fragments off thin links automatically — no manual traffic engineering.

Probes

Every link, on its own clock

Default cadence 250 ms per (peer, link) — RTT smoothed by EWMA (α = 0.2), jitter as windowed deviation, loss over a sliding window, dead after 5 consecutive misses. And a link is never alive before its first completed round-trip: recovery requires evidence, not hope.

Shipped · opt-in

Per-link probe intervals

[[link]] probe_interval_ms overrides the global clock per link, with timeout detection at 2× that link’s own interval. The reason it exists: probing a geostationary VSAT (~600 ms RTT) at 250 ms stacks probes inside the path latency and reads phantom loss — so the 4G link keeps 250 ms while the VSAT beside it checks at 1000 ms.

Data loss

Measured on real traffic, not just probes

Sequence numbers piggyback on every Nth data fragment (default 10), so under bursty interference the loss the actual traffic experiences is measured directly — sparse control probes systematically underestimate it.

Radio truth

Telemetry from the hardware itself

Adapters feed live RSSI, noise floor, buffer depth and error counters straight from the radio — signal a UDP probe can’t see. RSSI is the one the scheduler acts on, as an EWMA level and slope; the rest are there to be read, on the dashboard and in the API. adapters ↗

Budgets

A metered link that knows what it has left

[[link]] quota = "20 GB", with quota_period, quota_reset_day and quota_warn_pct, gives a link a byte budget. Once it is spent the scheduler stops promoting that link while any other link is usable — but it is still used when it is the last path left, because a budget is an economic constraint, not a hard-down. Usage, remaining budget, reset time and an over-budget flag are exported per link.

The timing truth, once and clearly. At defaults, hard dead-link detection is ≈1.25 s (5 × 250 ms). The sub-second headline — typically configured 0.1–0.75 s — is the reweighting cadence: how fast load shifts as quality degrades, long before anything is declared dead. Faster profiles (e.g. 50 ms × 3 ≈ 150 ms detection) are real but tuned. Tuned profile
Scheduling

Eight strategies, one contract: decide per packet.

Strategy is deployment policy, not vendor magic — chosen in one config line, observable in the decision log. Five of the eight are in the simulation below.

Strategy comparison — one stream, three dissimilar linksSimulation
Strategy

One copy per packet, on the path with the lowest size/capacity + RTT — so a bulky fragment prefers the fat link while a small control packet can take the low-latency one.

lowest_delivery_timestrategy in force
0%redundancy overhead
0packets in flight
Reading the lanes: the bar is each link’s live share of the stream and the shaded area under it is that link’s share over the last twelve seconds — so changing the strategy shows up as a step in all three at once. Latency-adaptive is the live demo mode: inject delay across the paths and watch redundancy escalate 1 → 2 → 3 simultaneous paths, then step back down only after the dwell — escalation is instant, de-escalation is deliberate.

The full set

lowest_delivery_timedefault

Per fragment, minimise size/capacity + RTT — a bulky fragment prefers the fat link, a tiny control packet may take the low-latency one. Capacity- and size-aware bonding without configuration.

weighted_round_robin

Stripe fragments across alive links proportionally to per-link weight — true aggregation when the links deserve it (the demo fleet weights its mesh : ad-hoc : Bluetooth ≈ 70 : 28 : 1.4 so the thin link never bottlenecks the bond).

broadcast

Every fragment on every alive link; the receiver’s anti-replay window deduplicates. Costs N× bandwidth, survives N−1 failures — and turns loss multiplicative: three independent links at 2% each yield 0.0008% effective loss; the scripted demo delivers ~2.7% through three links each forced to 30%. Measured

priority

Strict failover through an explicit priority_order — the primary carries everything until it dies. For pairs like a preferred serial radio with a second one as pure standby.

shortest_path

Switches multi-hop SPF to hop-count costs; per-fragment link choice falls back to delivery time. For meshes where hop count is the honest metric.

adaptiveescalating

Cost-ratio-driven redundancy with Schmitt-trigger hysteresis: mirror when the second link’s cost approaches the best (enter 0.9 / exit 0.5), triplicate deeper (0.5 / 0.25), 5 s dwell blocking de-escalation only. Cost inflates geometrically with loss (1/(1−loss)) and jitter.

loss_adaptiveescalating

The same ladder driven purely by measured loss: two paths at 20% loss, three at 50%, de-escalation at 0.7× the entry threshold.

latency_adaptiveescalating · live demo mode

Escalation driven by the best link’s RTT: mirror at 40 ms, triplicate at 100 ms, shared dwell. This is the interference-adaptive mode demonstrated live — inject delay, watch redundancy engage, watch it stand down.

Plus the redundancy_factor modifier — “send on the best N links” — for topping up any non-adaptive strategy.

Graceful degradation

Not binary. Continuous.

Real links don’t die instantly; they rot. Threshold failover keeps trusting a rotting link, then drops everything at once. Continuous scoring moves the traffic out while the link still carries.

A VSAT link degrading and recovering — gradual reweighting vs threshold failoverSimulation
steady state — VSAT 580 ms RTT · 0.2% loss · quality 92/100
t+0.0 sscenario clock — the 44-second cycle repeats
92 / 100measured quality of the link
30%share — Nexus Atlas, continuous
30%share — threshold failover
Why the share never quite reaches zero: a trickle of probes keeps measuring the degraded link, so recovery is detected the moment it begins — and rebuilt sample by sample, which is why there is no flapping on the way back up.
Range

The split follows the geometry.

Nothing is cut and no policy changes here — the aircraft simply flies further out. Each bearer’s usable rate falls at its own rate, so the scheduler reweights continuously; the short-reach fat link fades out of the bond long before the long-reach narrow one does.

Ground station to UAV — three bearers, one bond, distance the only variableSimulation
Stand-off
2.0 kmdistance from the ground station
3 of 3bearers carrying traffic
99.6 Mbpsaggregate usable rate
What you’re watching: reach and capacity run in opposite directions — the 5 GHz link is the fat one and the first to run out of range, the 900 MHz link is the thin one still working at the far end. The curves are each bearer’s usable rate against distance (log scale); the bar underneath is the share that rate earns it. All three carry at once, and no moment in the sweep is a failover.
config.toml — two links, adaptive scheduling
# A 4G modem beside a geostationary VSAT — each probed on its own clock.
[[link]]
name = "wwan0"
bind_device = "wwan0"
capacity = "50 Mbps"

[[link]]
name = "vsat0"
bind_device = "eth1"
capacity = "10 Mbps"
probe_interval_ms = 1000   # don’t stack probes inside 600 ms of path latency

[scheduler]
strategy = "latency_adaptive"
latency_mirror_ms = 40
latency_triplicate_ms = 100

[link_monitor]
probe_interval_ms = 250
dead_probe_count = 5

Then atlasd config check proves it loadable before any restart window, and atlasd monitor shows the decisions live.

Request an evaluation

Your worst link deserves better company.

Bring the bearer mix you actually have — VSAT beside LTE beside a 64 kbps serial radio — and watch one daemon make sense of it.