NetPilot
Back to Blog
Tutorial••8 min

MikroTik Load Balancing with PCC: Dual WAN Lab on RouterOS 7

Build PCC load balancing with failover on RouterOS 7 in a lab: routing tables, mangle rules, masquerade and check-gateway, with a measured 10 to 10 split and 22 second failover.

S
Sarah Chen
Network Engineer

MikroTik load balancing with PCC splits new connections across two WAN links, and it keeps every packet of a connection on the same link so sessions survive. Most guides stop at the config. A lab lets you prove the two things that matter: that traffic really leaves on both WANs, and that it fails over when one dies.

This lab does both on RouterOS 7.24.5. In our run 20 connections split 10 and 10 across the WANs, failover took about 22 seconds, and recovery took about 8. The files are in our public mikrotik-labs repository.

PCC, ECMP and nth at a glance

MethodDecides perKeeps a session on one WANSetupBest for
PCCConnection, by hash✅ Yes⚠️ Mangle rules and routing tablesDual WAN for users, stable sessions
ECMPRoute, several gateways⚠️ Depends on the hash policy✅ One routeSimple equal links
nthEvery nth connection✅ With connection marks⚠️ Mangle rulesStrict rotation

Bottom line: PCC is the usual choice for two internet links, because connections stay put. Test it in a lab first, since a wrong mark or a missing return path breaks traffic in ways that are hard to read on a live router. NetPilot builds this lab from a prompt in about 2 minutes, and you verify it on the real RouterOS CLI.

The lab

                ┌─ ether2 10.1.0.2 ─ isp1 ─┐
pc1 ─ ether4 ─ gw                          web 203.0.113.10
                └─ ether3 10.2.0.2 ─ isp2 ─┘

gw balances. isp1 and isp2 are the two providers. web is reachable through both, so its logs show which WAN a connection used.

Build it with the agent

"Build a MikroTik dual WAN lab: a CHR gateway with two uplinks to two CHR ISP routers, both reaching one Linux web server, and a Linux PC on the LAN. Configure PCC load balancing with check-gateway failover on RouterOS 7, then prove traffic uses both WANs and fail one over."

The agent designs the topology, writes the RouterOS config, deploys, and runs the checks. Direct CLI is always available too: open a terminal on gw and read the mangle counters yourself.

Or run it yourself

On a Linux host with containerlab v0.82.0 or newer:

git clone https://github.com/netpilot-labs/mikrotik-labs
cd mikrotik-labs/03-dual-wan-pcc
sudo containerlab deploy -t dual-wan-pcc.clab.yml

The config, step by step

This is the gw.rsc startup config from the lab, in order.

1. Addresses and routing tables

/ip address add address=10.1.0.2/30 interface=ether2 comment=WAN1
/ip address add address=10.2.0.2/30 interface=ether3 comment=WAN2
/ip address add address=192.168.88.1/24 interface=ether4 comment=LAN
/routing table add name=to_wan1 fib
/routing table add name=to_wan2 fib

On RouterOS 7 the routing table has to exist before a route or a mangle rule can use it. The fib flag is what puts it in the forwarding path.

2. Keep inbound connections on their WAN

/ip firewall mangle add chain=prerouting in-interface=ether2 connection-mark=no-mark action=mark-connection new-connection-mark=wan1_conn passthrough=yes
/ip firewall mangle add chain=prerouting in-interface=ether3 connection-mark=no-mark action=mark-connection new-connection-mark=wan2_conn passthrough=yes

A connection that arrives on a WAN must answer on the same WAN. These two rules make sure of that.

3. Classify LAN connections

/ip firewall mangle add chain=prerouting in-interface=ether4 dst-address-type=!local connection-mark=no-mark per-connection-classifier=both-addresses-and-ports:2/0 action=mark-connection new-connection-mark=wan1_conn passthrough=yes
/ip firewall mangle add chain=prerouting in-interface=ether4 dst-address-type=!local connection-mark=no-mark per-connection-classifier=both-addresses-and-ports:2/1 action=mark-connection new-connection-mark=wan2_conn passthrough=yes

2/0 and 2/1 mean two buckets. both-addresses-and-ports feeds source and destination address and port into the hash, so two connections to one server can land on different WANs. dst-address-type=!local leaves traffic for the router itself alone.

4. Turn connection marks into routing marks

/ip firewall mangle add chain=prerouting in-interface=ether4 connection-mark=wan1_conn action=mark-routing new-routing-mark=to_wan1 passthrough=no
/ip firewall mangle add chain=prerouting in-interface=ether4 connection-mark=wan2_conn action=mark-routing new-routing-mark=to_wan2 passthrough=no
/ip firewall mangle add chain=output connection-mark=wan1_conn action=mark-routing new-routing-mark=to_wan1 passthrough=no
/ip firewall mangle add chain=output connection-mark=wan2_conn action=mark-routing new-routing-mark=to_wan2 passthrough=no

5. NAT and routes with failover

/ip firewall nat add chain=srcnat out-interface=ether2 action=masquerade
/ip firewall nat add chain=srcnat out-interface=ether3 action=masquerade
/ip route add dst-address=0.0.0.0/0 gateway=10.1.0.1 routing-table=to_wan1 check-gateway=ping
/ip route add dst-address=0.0.0.0/0 gateway=10.2.0.1 routing-table=to_wan1 distance=2
/ip route add dst-address=0.0.0.0/0 gateway=10.2.0.1 routing-table=to_wan2 check-gateway=ping
/ip route add dst-address=0.0.0.0/0 gateway=10.1.0.1 routing-table=to_wan2 distance=2
/ip route add dst-address=0.0.0.0/0 gateway=10.1.0.1 check-gateway=ping
/ip route add dst-address=0.0.0.0/0 gateway=10.2.0.1 distance=2

Each table has its own WAN at distance 1 and the other WAN at distance 2. That second route is the failover.

Your SSH session is safe through all of this in containerlab, because management lives in its own routing table.

Prove the split

Send 20 connections from pc1:

docker exec clab-dual-wan-pcc-pc1 sh -c 'for i in $(seq 1 20); do curl -s -m3 -o /dev/null http://203.0.113.10/; done'

A packet capture on web shows where each SYN arrived. In this run 10 came in on eth1 from 10.1.0.2 and 10 on eth2 from 10.2.0.2:

18:19:30.689925 eth1  In  IP 10.1.0.2.38704 > 203.0.113.10.80: Flags [S]
18:19:30.694241 eth1  In  IP 10.1.0.2.38706 > 203.0.113.10.80: Flags [S]
18:19:30.698581 eth2  In  IP 10.2.0.2.38718 > 203.0.113.10.80: Flags [S]

PCC is a hash, not a rotation, so do not expect a perfect split every time. In an earlier run of 21 connections the same rules gave 7 and 14. The mangle counters show it on the router:

[admin@gw] > /ip firewall mangle print stats
Columns: CHAIN, ACTION, BYTES, PACKETS
# CHAIN       ACTION           BYTES  PACKETS
0 prerouting  mark-connection    240        2
1 prerouting  mark-connection    240        2
2 prerouting  mark-connection    444        7
3 prerouting  mark-connection    840       14
4 prerouting  mark-routing     2 856       44
5 prerouting  mark-routing     6 272       98
6 output      mark-routing         0        0
7 output      mark-routing         0        0

Rules 2 and 3 count new connections per WAN. Rules 4 and 5 count every packet of those connections.

Prove the failover

Take WAN1 down at the provider, so gw still sees its link up. That is the failure check-gateway exists for:

/interface disable ether2

Run that on isp1. About 22 seconds later the distance 1 routes for WAN1 go inactive and the backups take over:

[admin@gw] > /ip route print where dst-address=0.0.0.0/0 and routing-table!=mgmt
Flags: I - INACTIVE, A - ACTIVE; s - STATIC
Columns: DST-ADDRESS, GATEWAY, ROUTING-TABLE, DISTANCE
#    DST-ADDRESS  GATEWAY   ROUTING-TABLE  DISTANCE
0 Is 0.0.0.0/0    10.1.0.1  main                  1
1 As 0.0.0.0/0    10.2.0.1  main                  2
4 Is 0.0.0.0/0    10.1.0.1  to_wan1               1
5 As 0.0.0.0/0    10.2.0.1  to_wan1               2
6 As 0.0.0.0/0    10.2.0.1  to_wan2               1
7  s 0.0.0.0/0    10.1.0.1  to_wan2               2

All 20 new connections from pc1 then answered with HTTP 200. Enable ether2 on isp1 again and the routes moved back after about 8 seconds. Expect those 22 seconds to hit users during a real outage, and test any change to the ping target or timers the same way.

What this lab does not measure

The CHR free license limits each interface to 1 Mbps upload, as MikroTik documents, so this lab proves behavior and not bandwidth. Connection marks, routes and failover timing all work the same at any speed the lab can show, and that is the part worth testing.

FAQ

How does PCC load balancing work on MikroTik?

Per Connection Classifier hashes fields of each new connection, such as addresses and ports, and a mangle rule marks the connection for one WAN according to the hash result. Every packet of that connection then uses the same WAN, so sessions do not break. It is a hash, not round robin, so the split is close to even over many connections and can lean to one side over a few.

What changed for PCC on RouterOS 7?

On RouterOS 7 you create the routing tables first with /routing table add name=to_wan1 fib, and a route selects its table with routing-table=to_wan1. The mangle rules keep the same shape as before.

Not for a single connection. PCC keeps each connection on one WAN, so one download uses one link. The gain comes across many connections, which spread over both links.

How fast is MikroTik failover with check-gateway=ping?

In our lab the route moved to the backup about 22 seconds after the upstream interface went down, and moved back about 8 seconds after it returned. Connections marked for the failed WAN used the backup route with no change to the mangle rules.

Next steps

Start with CHR in containerlab if the setup is new to you, or practice the basics in the MTCNA lab. The MikroTik lab page covers the hosted route, and MikroTik's own load balancing documentation covers the other methods. To rehearse your own WAN change, describe it at app.netpilot.io.

NetPilot is not affiliated with MikroTik. MikroTik and RouterOS are trademarks of their owner.

Try NetPilot Free

Build enterprise-grade network labs in seconds with AI assistance

Get Started Free