← Tech docs

Primary and fallback rebalance routes

June 2026

When several channels are depleted at once, the planner does not fire a single rebalance and hope. It lays out a full list of routes up front — one or more primaries per depleted channel, plus fallbacks — and lets execution walk that list. This note answers how the list is built and how it converges. The budgeting note covers what happens once a route is actually attempted.

What's a target and what's a source?

At plan time every channel is bucketed by balance: a target is depleted (local below the 20% threshold) and needs inbound; a source is overfull (local above 80%) and can give. The two lists are sorted in opposite directions — targets most-depleted first, sources most-overfull first — so the deepest need is matched against the fullest giver.

How are primary pairs chosen?

The planner walks the targets deepest-first and gives each the fullest sources not already spoken for, until that target's deficit is covered. A single source rarely has enough surplus alone, so several sources can each contribute their share to one target:

for target in targets (deepest first):
    for source in sources (fullest first):
        take min(remaining_deficit, source_surplus)
        reserve that surplus, reduce the deficit

Committed surplus is tracked so no source is double-spent across targets, and a (source, target) pair is only ever used once. It is deliberately a greedy pass, not a global cost-minimising assignment — the ordering is predictable and easy to read, and the fallbacks absorb the cases where a greedy pick falls short.

What are the fallback pairs?

After the primaries, the planner appends a fallback for every other target × source combination not used as a primary, still in most-overfull order:

for target in targets:
    for src in sources not already paired with target:
        plans += fallback(target, src)   # is_fallback = true

These are not "retry if the primary failed" branches. They are simply later entries in the same flat list, attempted in order only while their target still has a deficit at run time — typically because the primary pair had no viable route between those two specific nodes.

So how does execution decide which actually fire?

Two running ledgers, carried through the whole run, with no conditional logic in the planner at all:

  • target_deficits — sats each depleted target still needs;
  • source_remaining — sats each overfull source can still send.

Every plan is capped at min(plan amount, target deficit, source remaining) before being attempted, and both ledgers decrement on success. A plan only fires while its target's deficit is still ≥ 50k and its source still has ≥ 50k to send — both conditions emerge from the ledgers, no separate gating. The planner attaches target_total_deficit and source_total_surplus to each plan so the executor needs no extra LND calls.

What does that bookkeeping give you for free?

  • A primary that partially fills its target leaves the deficit open, and the next plan for it (often a fallback) picks up where it left off without overshooting.
  • A source drained by an earlier successful plan auto-skips the rest of its plans, avoiding wasted insufficient-balance attempts.
  • A target fully filled removes itself from contention; remaining fallbacks for it are skipped.

Can you walk a concrete example?

Target T needs 600k; source A has 500k of surplus, source B has 1M.

plan 1  A → T   moves 500k  → A exhausted, T still needs 100k
plan 2  B → T   (fallback) fires because T's deficit > 0
                moves 100k  → T's deficit closes

There is no branching logic; the ledgers carry the state and the plan list is just walked in order. With two targets and two sources the planner emits the primaries T1←A, T2←B and the cross fallbacks T1←B, T2←A — so if a primary route has no path, the cross pairing tops the target up, and not one line of "what if" was needed to arrange it.

Why a flat list instead of branches?

  • Resilience — each target gets several shots at being filled from different sources within one run.
  • Predictability — order is fully determined by depth and fullness, so you can read the plan and know exactly what will be attempted and in what sequence.
  • Simplicity — the planner is pure list-building
Like the thinking? Run it on your own node.