PCC Load Balancing: Per-Connection Classifier
PCC Load Balancing: Per-Connection Classifier
Section titled “PCC Load Balancing: Per-Connection Classifier”Per-Connection Classifier (PCC) distributes outbound connections across multiple WAN links while keeping each individual connection on the same gateway for its entire lifetime. Unlike ECMP (which assigns individual packets round-robin), PCC assigns each connection to a WAN at the moment it is created and connection tracking remembers that assignment for all subsequent packets in that flow.
How PCC Works
Section titled “How PCC Works”PCC hashes selected fields from the packet header — source address, destination address, source port, destination port, or any combination — and maps the result into a bucket. You divide the buckets among your WAN links; new connections landing in a given range of buckets are marked for the corresponding link.
Because connection tracking propagates the mark to all ESTABLISHED and RELATED packets, both directions of the flow follow the same path (symmetric routing). The hash stays constant throughout the life of the connection, so there are no mid-session gateway changes.
PCC matcher syntax:
per-connection-classifier=<hash-fields>:<total-buckets>/<bucket-id>| Option | Description |
|---|---|
both-addresses | Hash src IP + dst IP |
both-ports | Hash src port + dst port |
src-address | Hash src IP only |
dst-address | Hash dst IP only |
both-addresses-and-ports | Hash all four fields (recommended) |
<total>/<id> | Bucket count and zero-based bucket index |
<total>/<id1>,<id2>,... | Multiple buckets assigned to one mark (weighted) |
Network Topology
Section titled “Network Topology”Examples in this guide use the following layout:
LAN (bridge, 192.168.88.0/24) ↕MikroTik Router ├── ether1 → ISP1, gateway 203.0.113.1 (WAN1) └── ether2 → ISP2, gateway 198.51.100.1 (WAN2)2-WAN Equal Load Balancing
Section titled “2-WAN Equal Load Balancing”Step 1 — Create Routing Tables
Section titled “Step 1 — Create Routing Tables”RouterOS needs a dedicated routing table for each WAN so that marked packets can be forced through the correct gateway.
/routing tableadd name=wan1 fibadd name=wan2 fibStep 2 — Add Per-Table Default Routes
Section titled “Step 2 — Add Per-Table Default Routes”/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1 routing-table=wan1 check-gateway=pingadd dst-address=0.0.0.0/0 gateway=198.51.100.1 routing-table=wan2 check-gateway=pingAlso add a main-table default route pair for traffic that does not carry a routing mark (e.g. the router itself):
/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1 distance=1 check-gateway=pingadd dst-address=0.0.0.0/0 gateway=198.51.100.1 distance=2 check-gateway=pingStep 3 — Mangle Rules
Section titled “Step 3 — Mangle Rules”The Mangle chain does two things for each connection:
mark-connection— assigns a connection mark based on the PCC bucket (runs once, on the firstNEWpacket).mark-routing— maps the connection mark to a routing mark (runs on every packet includingESTABLISHED).
/ip firewall mangle
# --- WAN1: bucket 0 of 2 ---add chain=prerouting action=mark-connection new-connection-mark=wan1_conn \ passthrough=yes \ per-connection-classifier=both-addresses-and-ports:2/0 \ connection-state=new \ in-interface=bridge comment="PCC LAN->WAN1"
add chain=prerouting action=mark-routing new-routing-mark=wan1 \ passthrough=no \ connection-mark=wan1_conn \ in-interface=bridge comment="Route WAN1 traffic"
# --- WAN2: bucket 1 of 2 ---add chain=prerouting action=mark-connection new-connection-mark=wan2_conn \ passthrough=yes \ per-connection-classifier=both-addresses-and-ports:2/1 \ connection-state=new \ in-interface=bridge comment="PCC LAN->WAN2"
add chain=prerouting action=mark-routing new-routing-mark=wan2 \ passthrough=no \ connection-mark=wan2_conn \ in-interface=bridge comment="Route WAN2 traffic":::info New connections only
The connection-state=new filter on the mark-connection rules ensures the PCC hash is evaluated only when a connection is created. Subsequent packets (ESTABLISHED/RELATED) inherit the mark from connection tracking and are handled by the mark-routing rules without re-hashing.
:::
Step 4 — NAT Masquerade
Section titled “Step 4 — NAT Masquerade”Each WAN interface needs its own masquerade rule so outbound packets carry the correct source address.
/ip firewall natadd chain=srcnat action=masquerade out-interface=ether1 comment="Masq WAN1"add chain=srcnat action=masquerade out-interface=ether2 comment="Masq WAN2"3-WAN Weighted Distribution
Section titled “3-WAN Weighted Distribution”Use multiple bucket IDs in a single rule to give one link a larger share of connections. The example below splits 10 total buckets: ISP1 gets 50%, ISP2 gets 30%, ISP3 gets 20%.
/routing tableadd name=wan1 fibadd name=wan2 fibadd name=wan3 fib
/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1 routing-table=wan1 check-gateway=pingadd dst-address=0.0.0.0/0 gateway=198.51.100.1 routing-table=wan2 check-gateway=pingadd dst-address=0.0.0.0/0 gateway=192.0.2.1 routing-table=wan3 check-gateway=ping
/ip firewall mangle
# ISP1 — buckets 0-4 (50%)add chain=prerouting action=mark-connection new-connection-mark=wan1_conn \ passthrough=yes connection-state=new \ per-connection-classifier=both-addresses-and-ports:10/0,1,2,3,4
add chain=prerouting action=mark-routing new-routing-mark=wan1 \ passthrough=no connection-mark=wan1_conn in-interface=bridge
# ISP2 — buckets 5-7 (30%)add chain=prerouting action=mark-connection new-connection-mark=wan2_conn \ passthrough=yes connection-state=new \ per-connection-classifier=both-addresses-and-ports:10/5,6,7
add chain=prerouting action=mark-routing new-routing-mark=wan2 \ passthrough=no connection-mark=wan2_conn in-interface=bridge
# ISP3 — buckets 8-9 (20%)add chain=prerouting action=mark-connection new-connection-mark=wan3_conn \ passthrough=yes connection-state=new \ per-connection-classifier=both-addresses-and-ports:10/8,9
add chain=prerouting action=mark-routing new-routing-mark=wan3 \ passthrough=no connection-mark=wan3_conn in-interface=bridge
/ip firewall natadd chain=srcnat action=masquerade out-interface=ether1add chain=srcnat action=masquerade out-interface=ether2add chain=srcnat action=masquerade out-interface=ether3Symmetric Routing
Section titled “Symmetric Routing”Symmetric routing means both directions of a connection use the same WAN link:
- Outbound (LAN → Internet): the
mark-routingrule inpreroutingdirects the packet to the correct routing table. - Inbound / reply (Internet → LAN): reply packets arrive on the WAN interface they were sent through. Connection tracking recognises the ESTABLISHED flow and the
mark-routingrule re-applies the same routing mark, which causes RouterOS to use the routing table for that WAN.
This is automatic as long as:
- The
mark-connectionrules fire only onNEWpackets (preventing re-hashing of established flows). - The
mark-routingrules match onconnection-mark(notper-connection-classifier) so they apply to ESTABLISHED packets too. - Each WAN interface has a masquerade NAT rule, ensuring the source address is appropriate for the ISP.
Avoiding Connection Drops on Failover
Section titled “Avoiding Connection Drops on Failover”PCC connections fail when a WAN link drops because the marked routing table no longer has a reachable route. The following techniques reduce the impact:
check-gateway on Per-Table Routes
Section titled “check-gateway on Per-Table Routes”Add check-gateway=ping to each routing-table default route. RouterOS periodically pings the gateway; when it becomes unreachable the route is removed from the table.
/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1 routing-table=wan1 check-gateway=pingadd dst-address=0.0.0.0/0 gateway=198.51.100.1 routing-table=wan2 check-gateway=pingWhen a routing-table route disappears, packets with that routing mark have nowhere to go. Existing connections on the failed link will drop. New connections will not start (the PCC hash still points to the dead table). To redirect new connections, flush the connection marks for the failed WAN using a Netwatch script or manual /ip firewall connection remove:
# Example: run in a Netwatch down-script for ISP1/ip firewall connection remove [find connection-mark=wan1_conn]This forces those sessions to start fresh on the next NEW packet and pick up a route from the remaining active WAN via the main-table fallback.
Main-Table Fallback Routes
Section titled “Main-Table Fallback Routes”Always keep a pair of distance-prioritised routes in the main routing table. These serve traffic from the router itself and act as a catch-all when PCC-marked routing tables lose their routes.
/ip routeadd dst-address=0.0.0.0/0 gateway=203.0.113.1 distance=1 check-gateway=pingadd dst-address=0.0.0.0/0 gateway=198.51.100.1 distance=2 check-gateway=pingExclude Local and Management Traffic from PCC
Section titled “Exclude Local and Management Traffic from PCC”Prevent management sessions from being load-balanced, which can cause unexpected loss of access when a WAN drops:
/ip firewall mangleadd chain=prerouting action=accept \ dst-address=<router-management-ip> \ comment="Skip PCC for management" place-before=0Verification
Section titled “Verification”# Active connections and their marks/ip firewall connection print where connection-mark~"wan"
# Routing tables/ip route print where routing-table=wan1/ip route print where routing-table=wan2
# Mangle counters (confirm rules are matching)/ip firewall mangle print statsExpected output for a working 2-WAN setup: both wan1_conn and wan2_conn connection marks are present, and the per-table routes are active (not flagged as unreachable).
Limitations
Section titled “Limitations”| Limitation | Detail |
|---|---|
| Single-host penalty | A single client talking to many servers gets good distribution. A single client with a single destination may always hash to the same WAN. Use src-address-and-dst-port or both-addresses-and-ports to improve spread. |
| Existing session drops on failover | When a WAN drops, all connections on that link drop. PCC does not migrate sessions. |
| Server-side inbound traffic | PCC affects outbound routing only. Inbound traffic (hosted services) depends on DNS / BGP, not PCC. |
| IPv6 | PCC rules must be duplicated in /ipv6 firewall mangle for IPv6 traffic. |