The naive multi-WAN setup — two default routes, different distances, check-gateway=ping on both — fails in exactly the scenario it was built for. check-gateway pings the next hop: your ISP’s CPE sitting in the same rack. When the ISP’s upstream dies, that CPE still answers pings cheerfully while routing your packets into a black hole. Your “failover” never fires, and you learn about the outage from users, not from the router.
Real failover means testing reachability through the link to something that isn’t your ISP’s first hop. RouterOS has done this for years with a trick called recursive routing, and RouterOS 7 made the mechanics cleaner. Load balancing is a separate problem with a separate tool — PCC — and the two compose nicely once you respect their caveats. Let’s build both.
Topology for this article: WAN1 on ether1 (gateway 203.0.113.1), WAN2 on ether2 (gateway 198.51.100.1), LAN 10.0.10.0/24 on bridge-lan.
Recursive Failover
The idea: create a host route to a reliable public IP (say 1.1.1.1) via WAN1, then point the default route’s gateway at 1.1.1.1 itself. The default route only stays active if 1.1.1.1 is resolvable and answering — which requires the entire WAN1 path to work, not just the first hop.
The mechanics run on scope and target-scope. Every route has a scope (default 30 for static; connected routes get 10). A route’s gateway resolves recursively through routes whose scope is ≤ its own target-scope. So: host routes to the ping targets get scope=10, and the default routes get target-scope=11 — allowing them to resolve through those host routes (and connected routes), but nothing else.
/ip route# Host routes pinning each probe target to one specific WANadd dst-address=1.1.1.1/32 gateway=203.0.113.1 scope=10 \ comment="probe target via WAN1"add dst-address=9.9.9.9/32 gateway=198.51.100.1 scope=10 \ comment="probe target via WAN2"
# Recursive defaults — gateway is the probe target, not the ISP routeradd dst-address=0.0.0.0/0 gateway=1.1.1.1 check-gateway=ping \ distance=1 target-scope=11 comment="default via WAN1"add dst-address=0.0.0.0/0 gateway=9.9.9.9 check-gateway=ping \ distance=2 target-scope=11 comment="default via WAN2"Now check-gateway=ping probes 1.1.1.1 through WAN1 (the host route forces the path). Two consecutive missed probes (checks run every 10 seconds) and the route goes inactive; the distance-2 route takes over; when pings succeed again, WAN1 resumes. End-to-end failover in roughly 20–30 seconds, no scripts.
Two operational notes. First, use different probe targets per WAN — anycast IPs like 1.1.1.1 are globally reliable but a single target shared across both WANs means one bad peering event fails both routes. Better yet, belt-and-suspenders with two probe targets per WAN (two host routes, two recursive defaults at the same distance). Second, those host routes mean 1.1.1.1 is always reached via WAN1 even after failover — so don’t pick your monitoring or DNS target as a probe IP unless you’ve thought that through.
Verify:
/ip route print detail where dst-address=0.0.0.0/0# active route shows: gateway=1.1.1.1 recursive via 203.0.113.1# the inactive one sits with distance=2 waiting
# Simulate failure without touching cables: blackhole the probe/ip route add dst-address=1.1.1.1/32 type=blackhole distance=1 scope=10# watch the default flip within ~20s, then remove the blackhole/ip route remove [find type=blackhole]Netwatch: For Everything Routing Can’t Express
Recursive routes handle route selection. Netwatch handles side effects: alerting, flushing NAT state, disabling a VPN that shouldn’t run over the LTE backup, forcing DNS changes. RouterOS 7’s netwatch also does HTTPS and DNS probes, not just ICMP — so you can fail over on “the internet is technically up but DNS is dead,” which ICMP alone never catches.
/tool netwatchadd name=wan1-monitor host=1.1.1.1 interval=30s timeout=1s \ down-script=":log warning \"WAN1 down - failover engaged\";\ /ip firewall connection remove [find connection-mark=\"wan1\"]" \ up-script=":log warning \"WAN1 restored\""That connection-flush in the down-script matters more than it looks: established connections keep their conntrack state and NATed connections keep trying the dead path until they time out. Flushing forces clients to reconnect over the surviving WAN immediately instead of after minutes of TCP retries. (Scope the flush — flushing the whole table on a flap is a self-inflicted outage.)
One warning from experience: don’t build your primary failover logic in netwatch scripts that enable/disable routes. Scripted failover has failure modes routing doesn’t — scripts don’t run at startup ordering you expect, permissions bite (netwatch scripts run with limited policies), and a flapping link executes your scripts in rapid fire. Let recursive routes do route selection; let netwatch do notifications and cleanup.
PCC Load Balancing
Failover leaves WAN2 idle until disaster. PCC (per-connection classifier) uses both: it hashes fields of each connection and assigns the connection to a WAN based on the remainder. both-addresses:2/0 means “hash src+dst address, divide by 2, match remainder 0” — half your connections. Critically, it’s per-connection, so a flow never straddles two WANs mid-stream (which, with NAT, would break it instantly).
Routing tables first — in RouterOS 7 they must be created explicitly with fib:
/routing tableadd name=to-wan1 fibadd name=to-wan2 fibMangle does the classification. Three stages: keep inbound WAN traffic on the interface it arrived on, classify new LAN connections into two buckets, then route by connection mark:
/ip firewall mangle# Stage 1: traffic that arrived via a WAN answers via the same WANadd chain=input in-interface=ether1 action=mark-connection \ new-connection-mark=wan1 comment="PCC: inbound stickiness WAN1"add chain=input in-interface=ether2 action=mark-connection \ new-connection-mark=wan2add chain=output connection-mark=wan1 action=mark-routing \ new-routing-mark=to-wan1add chain=output connection-mark=wan2 action=mark-routing \ new-routing-mark=to-wan2
# Stage 2: split new LAN connections 50/50add chain=prerouting in-interface=bridge-lan dst-address-type=!local \ connection-state=new connection-mark=no-mark \ per-connection-classifier=both-addresses:2/0 \ action=mark-connection new-connection-mark=wan1 comment="PCC: bucket 0"add chain=prerouting in-interface=bridge-lan dst-address-type=!local \ connection-state=new connection-mark=no-mark \ per-connection-classifier=both-addresses:2/1 \ action=mark-connection new-connection-mark=wan2 comment="PCC: bucket 1"
# Stage 3: route by markadd chain=prerouting in-interface=bridge-lan connection-mark=wan1 \ dst-address-type=!local action=mark-routing new-routing-mark=to-wan1add chain=prerouting in-interface=bridge-lan connection-mark=wan2 \ dst-address-type=!local action=mark-routing new-routing-mark=to-wan2dst-address-type=!local keeps traffic aimed at the router itself out of the balancing. both-addresses keeps each src/dst pair pinned to one WAN — important for sites that dislike your IP changing between requests. both-addresses-and-ports:2/0 spreads more evenly but flips WANs between connections to the same site; banks and some SaaS logins will punish you for it.
Per-table routes (with their own recursive failover, so a dead WAN’s share fails over instead of blackholing) and NAT:
/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1 routing-table=to-wan1 \ check-gateway=pingadd dst-address=0.0.0.0/0 gateway=198.51.100.1 routing-table=to-wan1 \ check-gateway=ping distance=2add dst-address=0.0.0.0/0 gateway=198.51.100.1 routing-table=to-wan2 \ check-gateway=pingadd dst-address=0.0.0.0/0 gateway=203.0.113.1 routing-table=to-wan2 \ check-gateway=ping distance=2
/ip firewall natadd chain=srcnat out-interface=ether1 action=masqueradeadd chain=srcnat out-interface=ether2 action=masqueradeThe Caveats That Break PCC
Fasttrack again. Fasttracked packets skip most of the mangle chains — your routing marks stop being applied, and balanced connections collapse onto whichever path conntrack first saw. If you run PCC, disable the fasttrack rule. Yes, that costs CPU; PCC routers do more per-packet work by design. (Same villain as in the CAKE article — this is a pattern.)
NAT consistency. Masquerade by out-interface, never a single NAT rule for everything, and flush marked connections when a WAN flips (the netwatch trick above) — otherwise conntrack happily NATs established flows out an interface whose route no longer exists.
Local-origin and hairpin traffic. Router-originated traffic follows the main table unless output-chain marks say otherwise — VPN tunnels terminating on the router need pinning to one WAN (an output mangle rule matching the tunnel’s dst, or a host route in main).
When ECMP Is Enough
If what you actually want is “use both links, minimal config, failover included,” plain ECMP deserves a look before you build the mangle cathedral:
/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1,198.51.100.1 check-gateway=pingOne route, two gateways. RouterOS 7 hashes flows across both (per-flow, not per-packet — v7’s hashing keeps connections stable, a real improvement over the v6 round-robin days), and check-gateway drops a dead gateway out of the set. Combined with per-interface masquerade, this is a legitimate two-liner load balancer.
Choose ECMP when links are similar and you don’t need ratio control or policy (“guest VLAN uses WAN2 only”). Choose PCC when you need unequal splits (a :3/0,1 vs :3/2 bucket layout gives 2:1), per-source policy, or predictable interface assignment. Choose failover-only when the second link is metered LTE. Its next-hop-ping limitation applies to ECMP too — for full-path checking you’d layer the recursive trick onto each gateway.
Verification
/ip route print where routing-table=to-wan1 # marks land in the right table/ip firewall mangle print stats # PCC counters splitting ~evenly/ip firewall connection print where connection-mark=wan2 # flows actually marked/tool torch interface=ether1 # live rate per WAN/tool torch interface=ether2 # roughly balanced under loadThen the end-to-end test that actually matters: from two LAN hosts, check myip services — you should see different public IPs. Pull WAN1’s cable during a large download and a video call: the download dies and re-establishes via WAN2 (or survives, if the app resumes), the call re-routes within the check-gateway window.
Closing Thoughts
Multi-WAN on RouterOS is three separate tools, and the failures come from conflating them: recursive routes with check-gateway for trustworthy failover (never trust a next-hop ping), netwatch for side effects and alerting (never for primary route selection), PCC or ECMP for balancing (never with fasttrack enabled). Build failover first and live with it for a week before adding balancing — most “load balancing” requirements turn out to be “I don’t want outages,” which is the cheaper problem.
This closes the loop with the rest of the series: the VPNs from the WireGuard and IKEv2 articles need pinning to a WAN, and the CAKE shaper needs per-WAN queues once traffic splits. On the VyOS side I solved the same problem with failure detection and policy-based routing — different syntax, identical philosophy: test the path, not the port light.