Skip to content

Traffic Shaping Guide

RouterOS provides a layered QoS toolkit. This guide explains how the pieces fit together: how HTB creates a bandwidth hierarchy, how PCQ distributes capacity fairly across users, how CAKE eliminates bufferbloat without manual tuning, how burst handles short-term traffic spikes, and how firewall mangle classifies traffic into these structures — with complete ISP and home network examples.

:::note Interface names in examples All examples in this guide use ether1-WAN as the WAN interface name. RouterOS does not create this name by default — replace it with the actual interface name on your device (e.g. ether1). You can rename interfaces with /interface set ether1 name=ether1-WAN if you prefer descriptive names. :::

Before touching configuration, understand the four layers:

LayerToolWhat it controls
ClassificationFirewall mangleWhich traffic belongs to which class
HierarchyQueue Tree (HTB)How bandwidth is divided and shared between classes
FairnessPCQ queue typeHow bandwidth within a class is shared across users
LatencyCAKE / FQ-CoDelPacket scheduling to reduce bufferbloat and jitter

Traffic flows through these layers in order. Mangle marks a packet; the queue tree assigns it to a class; the leaf queue type determines how it competes with other flows in that class.

┌─────────────────────────────┐
Packet arrives ───► │ Mangle (classify & mark) │
└──────────────┬──────────────┘
│ packet-mark
┌──────────────▼──────────────┐
│ Queue Tree (HTB hierarchy) │
│ limit-at / max-limit │
│ priority │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Leaf queue type │
│ PCQ (fairness per user) │
│ CAKE (AQM + fairness) │
│ FIFO / SFQ (simple) │
└─────────────────────────────┘

Queue Tree cannot identify traffic by IP address — it relies entirely on packet marks from /ip firewall mangle. Traffic without a matching mark passes through the tree without entering any leaf queue.

Always use a two-step approach: mark the connection once, then derive packet marks from the connection mark. This is more efficient than reclassifying every packet independently.

/ip firewall mangle
# Step 1: mark the connection on the first new packet
add chain=prerouting connection-state=new in-interface=ether1-WAN \
action=mark-connection new-connection-mark=dl_conn passthrough=yes
# Step 2: mark every subsequent packet from that connection
add chain=prerouting connection-mark=dl_conn \
action=mark-packet new-packet-mark=dl_pkt passthrough=no

Traffic direction from the router’s point of view:

DirectionChainWhen
Download → LANpreroutingClassify as packets arrive from WAN
Upload → WANpostroutingClassify after routing decision, before WAN egress
Router’s own trafficoutput / inputFor shaping traffic to/from the router itself

:::warning FastTrack bypasses QoS RouterOS FastTrack accelerates established connections by bypassing the firewall and queue subsystem. Any traffic you want to shape must be excluded from FastTrack.

Exclude shaped connections before the FastTrack rule, not after:

/ip firewall filter
# Accept shaped traffic first — it passes without FastTrack
add chain=forward connection-mark=dl_conn,ul_conn action=accept
# FastTrack only the remaining unclassified established traffic
add chain=forward connection-state=established,related action=fasttrack-connection

:::

RouterOS supports a maximum of 4096 unique packet mark names. Do not generate one packet mark per subscriber — use PCQ (below) for per-user differentiation instead, and keep mark names to a manageable set of service classes.


HTB (Hierarchical Token Bucket) is the scheduling algorithm used by all RouterOS Queue Trees. It lets you define a tree of queues where each node has a guaranteed rate (limit-at) and a ceiling (max-limit). When a class is not using its allocation, siblings can borrow the spare bandwidth up to their own max-limit.

How limit-at, max-limit, and priority interact

Section titled “How limit-at, max-limit, and priority interact”
ParameterEffect
limit-atGuaranteed (committed) rate — protected even when the parent is congested
max-limitHard ceiling — traffic never exceeds this even when the parent has spare capacity
priority1 (highest) to 8 (lowest); higher-priority queues are serviced first within the same parent

When a parent queue is congested, RouterOS services children in this order:

  1. Any child below its limit-at (guaranteed traffic, regardless of priority)
  2. Children above their limit-at, ordered by priority (1 first)
  3. Within the same priority level, bandwidth is divided by weight if limit-at values are set

All Queue Trees need a root queue and a WAN interface attachment:

# Interface-attached roots — these are the shaping points
/queue tree
add name=upload parent=ether1-WAN max-limit=50M
add name=download parent=global max-limit=200M

:::tip global for download shaping global intercepts incoming traffic before it reaches any interface queue. Use it for the download (WAN → LAN) root so all arriving traffic is shaped regardless of which physical interface it arrives on. :::

/queue tree
# Upload root (50 Mbps uplink)
add name=up-root parent=ether1-WAN max-limit=50M
# Classes: guaranteed + ceiling
add name=up-voip parent=up-root packet-mark=up_voip limit-at=5M max-limit=50M priority=1
add name=up-data parent=up-root packet-mark=up_data limit-at=10M max-limit=50M priority=4
add name=up-bulk parent=up-root packet-mark=up_bulk limit-at=5M max-limit=50M priority=8
# Download root (200 Mbps downlink)
add name=dn-root parent=global max-limit=200M
add name=dn-voip parent=dn-root packet-mark=dn_voip limit-at=10M max-limit=200M priority=1
add name=dn-data parent=dn-root packet-mark=dn_data limit-at=100M max-limit=200M priority=4
add name=dn-bulk parent=dn-root packet-mark=dn_bulk limit-at=20M max-limit=200M priority=8

Mangle rules to match the marks:

/ip firewall mangle
# Upload classification
add chain=postrouting out-interface=ether1-WAN protocol=udp \
dst-port=5060,10000-20000 action=mark-packet new-packet-mark=up_voip passthrough=no
add chain=postrouting out-interface=ether1-WAN dscp=0 p2p=all \
action=mark-packet new-packet-mark=up_bulk passthrough=no
add chain=postrouting out-interface=ether1-WAN \
action=mark-packet new-packet-mark=up_data passthrough=no
# Download classification (mirror)
add chain=prerouting in-interface=ether1-WAN protocol=udp \
src-port=5060,10000-20000 action=mark-packet new-packet-mark=dn_voip passthrough=no
add chain=prerouting in-interface=ether1-WAN dscp=0 p2p=all \
action=mark-packet new-packet-mark=dn_bulk passthrough=no
add chain=prerouting in-interface=ether1-WAN \
action=mark-packet new-packet-mark=dn_data passthrough=no

PCQ (Per Connection Queue) dynamically creates a sub-queue for each unique classifier value. When 20 users share a 100 Mbps class, each active user gets approximately 5 Mbps — without writing 20 separate queue rules.

100 Mbps class with pcq-classifier=dst-address
├── User A (192.168.1.10) ────── 33 Mbps
├── User B (192.168.1.20) ────── 33 Mbps
└── User C (192.168.1.30) ────── 33 Mbps
When User C disconnects:
├── User A ────── 50 Mbps
└── User B ────── 50 Mbps

Inactive users do not consume queue resources. The share automatically adjusts as users become active or idle.

ClassifierUse when
dst-addressDownload fairness (each destination IP = one sub-queue)
src-addressUpload fairness (each source IP = one sub-queue)
src-address,dst-addressPer-flow fairness (pair-based isolation)
src-port, dst-portPer-service fairness within a class
/queue type
add name=pcq-down kind=pcq pcq-classifier=dst-address pcq-rate=0
add name=pcq-up kind=pcq pcq-classifier=src-address pcq-rate=0

pcq-rate=0 means active sub-queues share available bandwidth equally. Set a non-zero pcq-rate only when you want a hard per-user cap (e.g., 5M = each user is individually limited to 5 Mbps regardless of available capacity).

The most effective pattern for ISP-style QoS: HTB parent queues enforce class guarantees and ceilings; PCQ at the leaf distributes each class fairly across subscribers.

/queue type
add name=pcq-down kind=pcq pcq-classifier=dst-address pcq-rate=0
add name=pcq-up kind=pcq pcq-classifier=src-address pcq-rate=0
/queue tree
# Upload: HTB parent, PCQ leaf
add name=up-root parent=ether1-WAN max-limit=50M
add name=up-users parent=up-root packet-mark=up_data \
limit-at=40M max-limit=50M queue=pcq-up
# Download: HTB parent, PCQ leaf
add name=dn-root parent=global max-limit=200M
add name=dn-users parent=dn-root packet-mark=dn_data \
limit-at=150M max-limit=200M queue=pcq-down

This gives every user an equal share of the class bandwidth as a baseline, while the HTB parent ensures the class stays within its allocation.


Burst lets a queue temporarily exceed its max-limit to handle page loads, connection setup, and short downloads without raising the steady-state rate. RouterOS does not use a fixed burst duration — instead it uses an averaging window:

ParameterDescription
burst-limitPeak rate allowed during burst
burst-thresholdBurst activates while the moving average is below this; deactivates when average exceeds it
burst-timeWindow (seconds) over which the average rate is calculated

Burst is active when the measured average is below burst-threshold. As soon as sustained traffic pushes the average above burst-threshold, the queue drops back to max-limit.

Rate
│ burst-limit ──────────────
│ burst-threshold ─ ─ ─ ─ ─
│ max-limit ─────────────────
│ limit-at ─ ─ ─ ─ ─ ─ ─ ─
└──────────────────────────────► Time
▲ ▲
burst on average crosses threshold, burst off

A practical rule: set burst-threshold between limit-at and max-limit.

/queue tree
add name=dn-data parent=dn-root packet-mark=dn_data \
limit-at=20M max-limit=50M \
burst-limit=80M burst-threshold=35M burst-time=8s

In this example:

  • Guaranteed rate: 20 Mbps
  • Steady-state ceiling: 50 Mbps
  • Burst peak: 80 Mbps (allowed while the 8-second average stays below 35 Mbps)
  • Once the average reaches 35 Mbps, burst deactivates and the queue caps at 50 Mbps

CAKE (Common Applications Kept Enhanced) is an active queue management algorithm that eliminates bufferbloat and provides per-flow fairness without requiring a queue tree hierarchy. It combines shaping, scheduling, and AQM in a single queue type.

Use CAKE whenUse HTB+PCQ when
You want bufferbloat elimination with minimal configYou need guaranteed rates per service class
Home or SOHO with one linkISP/multi-tenant with tiered subscriber plans
You don’t need multi-class hierarchyYou need VoIP/data/bulk priority tiers
You want per-flow fairness automaticallyYou want per-subscriber IP fairness

CAKE and HTB+PCQ are complementary: you can use CAKE as the leaf queue type inside an HTB tree to get both hierarchy and AQM.

/queue type
add name=cake-home kind=cake cake-bandwidth=45M cake-rtt=100ms \
cake-overhead-scheme=pppoe-vcmux cake-diffserv=diffserv4

Set the overhead scheme to match your link encapsulation. This tells CAKE how many bytes each packet actually consumes on the wire, enabling accurate shaping:

Link typecake-overhead-scheme
Plain Ethernetethernet
PPPoE over Ethernet (VC-Mux)pppoe-vcmux
PPPoE over VDSL (PTM framing)pppoe-ptm
PPPoA over ATM (VC-Mux)pppoa-vcmux
PPPoA over ATM (LLC)pppoa-llc
DOCSIS cabledocsis
VLAN-tagged Ethernetether-vlan

If your link type is not listed, measure the actual overhead and set a numeric value: cake-overhead=8 (positive bytes added) or cake-overhead=-4 (negative for protocols that remove header bytes).

For VDSL/DSL: add ATM or PTM framing to account for cell/byte alignment:

/queue type
# VDSL with PTM framing and PPPoE encapsulation
add name=cake-vdsl kind=cake cake-bandwidth=17M cake-overhead-scheme=pppoe-ptm \
cake-rtt=100ms cake-diffserv=diffserv4

cake-rtt accepts a time duration (e.g., 100ms, 5ms). Set it to the typical round-trip time of your link. Too-small RTT causes over-aggressive drops during bursts; too-large RTT makes CAKE less responsive to congestion.

Link typeRecommended cake-rtt
Sub-millisecond internal networks1ms
Local network (< 1 ms)5ms
City-level (< 10 ms)20ms
Regional ISP (< 30 ms)60ms
General internet (~100 ms RTT)100ms
Cross-ocean links (~300 ms)300ms
Satellite links (~500 ms)500ms

Use 100ms for most home and ISP deployments.

CAKE organizes traffic into priority buckets called “tins” based on DSCP markings:

ModeTinsUse when
besteffort1 (all equal)DSCP markings are untrusted or absent
diffserv33 (Bulk / Best-Effort / Interactive)Simple 3-class prioritization
diffserv44 (Bulk / Best-Effort / Video / Voice)Home QoS with VoIP and streaming
diffserv88Fine-grained DSCP enforcement at ISP edge

Use besteffort if you cannot trust incoming DSCP markings (customer CPE may mark traffic arbitrarily). Use diffserv4 or higher only when markings are trusted or set by your own mangle rules.

cake-wash=yes clears DSCP markings from packets after CAKE has used them for tin classification. This prevents DSCP markings from affecting downstream equipment when CAKE is deployed at an administrative boundary (ISP edge, WAN handoff):

/queue type
add name=cake-isp-edge kind=cake cake-bandwidth=1G cake-diffserv=diffserv4 \
cake-wash=yes cake-rtt=100ms cake-overhead-scheme=ethernet

For home gateways where you want Wi-Fi APs to use DSCP for internal prioritization, set cake-wash=no (the default).

/queue type
add name=cake-dl kind=cake cake-bandwidth=190M cake-rtt=100ms \
cake-overhead-scheme=pppoe-vcmux cake-diffserv=diffserv4
add name=cake-ul kind=cake cake-bandwidth=18M cake-rtt=100ms \
cake-overhead-scheme=pppoe-vcmux cake-diffserv=diffserv4
/queue simple
add name=shape-download target=192.168.0.0/24 max-limit=0/0 \
queue=cake-dl/cake-ul

Set cake-bandwidth to 95–98% of your measured line rate to allow CAKE to shape before the ISP’s buffer fills. This is the key to eliminating bufferbloat.

Combine HTB structure with CAKE’s AQM at the leaf:

/queue type
add name=cake-data kind=cake cake-bandwidth=0 cake-rtt=100ms cake-diffserv=diffserv4
/queue tree
add name=dn-root parent=global max-limit=200M
add name=dn-voip parent=dn-root packet-mark=dn_voip limit-at=10M max-limit=200M priority=1
add name=dn-data parent=dn-root packet-mark=dn_data limit-at=100M max-limit=200M \
priority=4 queue=cake-data
add name=dn-bulk parent=dn-root packet-mark=dn_bulk limit-at=20M max-limit=200M priority=8

When cake-bandwidth=0 inside an HTB tree, CAKE acts as a pure scheduler/AQM without imposing its own rate limit (the HTB parent handles the rate limit).


Real-World Example 1: Home Network (50/10 Mbps)

Section titled “Real-World Example 1: Home Network (50/10 Mbps)”

Goal: VoIP and gaming always work during bulk transfers. Browsing gets burst. Bufferbloat eliminated.

# --- Queue types ---
/queue type
add name=pcq-dn kind=pcq pcq-classifier=dst-address pcq-rate=0
add name=pcq-up kind=pcq pcq-classifier=src-address pcq-rate=0
# --- Mangle classification ---
/ip firewall mangle
# Upload: VoIP
add chain=postrouting out-interface=ether1-WAN protocol=udp \
dst-port=5060,10000-20000 action=mark-packet new-packet-mark=up_voip passthrough=no
# Upload: P2P / bulk
add chain=postrouting out-interface=ether1-WAN p2p=all \
action=mark-packet new-packet-mark=up_bulk passthrough=no
# Upload: everything else
add chain=postrouting out-interface=ether1-WAN \
action=mark-packet new-packet-mark=up_data passthrough=no
# Download: VoIP
add chain=prerouting in-interface=ether1-WAN protocol=udp \
src-port=5060,10000-20000 action=mark-packet new-packet-mark=dn_voip passthrough=no
# Download: P2P / bulk
add chain=prerouting in-interface=ether1-WAN p2p=all \
action=mark-packet new-packet-mark=dn_bulk passthrough=no
# Download: everything else
add chain=prerouting in-interface=ether1-WAN \
action=mark-packet new-packet-mark=dn_data passthrough=no
# --- Queue tree ---
/queue tree
# Upload tree (10 Mbps uplink — set max-limit to ~95%)
add name=up-root parent=ether1-WAN max-limit=9500k
add name=up-voip parent=up-root packet-mark=up_voip limit-at=2M max-limit=9500k priority=1
add name=up-data parent=up-root packet-mark=up_data limit-at=5M max-limit=9500k priority=4 \
burst-limit=9M burst-threshold=6M burst-time=8s queue=pcq-up
add name=up-bulk parent=up-root packet-mark=up_bulk limit-at=1M max-limit=9500k priority=8 \
queue=pcq-up
# Download tree (50 Mbps downlink — set max-limit to ~95%)
add name=dn-root parent=global max-limit=47M
add name=dn-voip parent=dn-root packet-mark=dn_voip limit-at=5M max-limit=47M priority=1
add name=dn-data parent=dn-root packet-mark=dn_data limit-at=25M max-limit=47M priority=4 \
burst-limit=45M burst-threshold=30M burst-time=8s queue=pcq-dn
add name=dn-bulk parent=dn-root packet-mark=dn_bulk limit-at=5M max-limit=47M priority=8 \
queue=pcq-dn
# --- FastTrack exclusion (exclude shaped traffic) ---
/ip firewall filter
add chain=forward connection-mark=!no-mark action=accept comment="shaped traffic bypasses fasttrack"
add chain=forward connection-state=established,related action=fasttrack-connection

What this achieves:

  • VoIP has 2 Mbps / 5 Mbps guaranteed and gets priority in congestion
  • General browsing/streaming can burst to near-line-rate when link is idle, then settles to its allocation
  • P2P is constrained to leftover bandwidth
  • PCQ ensures no single device monopolizes the data or bulk class

Real-World Example 2: WISP / ISP Subscriber Management

Section titled “Real-World Example 2: WISP / ISP Subscriber Management”

Goal: Multiple subscribers on a shared 200 Mbps uplink. Two service tiers: Standard (20/5 Mbps) and Premium (50/20 Mbps). Fair sharing within each tier. VoIP prioritized across all subscribers.

# --- Address lists for service tiers ---
/ip firewall address-list
add list=tier-standard address=10.10.1.0/24
add list=tier-premium address=10.10.2.0/24
# --- PCQ types for per-subscriber fairness ---
/queue type
add name=pcq-std-dn kind=pcq pcq-classifier=dst-address pcq-rate=20M
add name=pcq-std-up kind=pcq pcq-classifier=src-address pcq-rate=5M
add name=pcq-prem-dn kind=pcq pcq-classifier=dst-address pcq-rate=50M
add name=pcq-prem-up kind=pcq pcq-classifier=src-address pcq-rate=20M
# --- Mangle classification ---
/ip firewall mangle
# VoIP — highest priority, classified first
add chain=prerouting in-interface=ether1-WAN protocol=udp \
src-port=5060,10000-20000 action=mark-packet new-packet-mark=dn_voip passthrough=no
add chain=postrouting out-interface=ether1-WAN protocol=udp \
dst-port=5060,10000-20000 action=mark-packet new-packet-mark=up_voip passthrough=no
# Standard tier — download
add chain=prerouting in-interface=ether1-WAN \
dst-address-list=tier-standard \
action=mark-packet new-packet-mark=dn_std passthrough=no
# Standard tier — upload
add chain=postrouting out-interface=ether1-WAN \
src-address-list=tier-standard \
action=mark-packet new-packet-mark=up_std passthrough=no
# Premium tier — download
add chain=prerouting in-interface=ether1-WAN \
dst-address-list=tier-premium \
action=mark-packet new-packet-mark=dn_prem passthrough=no
# Premium tier — upload
add chain=postrouting out-interface=ether1-WAN \
src-address-list=tier-premium \
action=mark-packet new-packet-mark=up_prem passthrough=no
# --- Queue tree ---
/queue tree
# Download root (200 Mbps shared downlink)
add name=dn-root parent=global max-limit=200M
add name=dn-voip parent=dn-root packet-mark=dn_voip limit-at=20M max-limit=200M priority=1
add name=dn-std parent=dn-root packet-mark=dn_std limit-at=60M max-limit=200M priority=4 \
queue=pcq-std-dn \
burst-limit=250M burst-threshold=150M burst-time=10s
add name=dn-prem parent=dn-root packet-mark=dn_prem limit-at=100M max-limit=200M priority=3 \
queue=pcq-prem-dn \
burst-limit=250M burst-threshold=150M burst-time=10s
# Upload root (100 Mbps shared uplink)
add name=up-root parent=ether1-WAN max-limit=100M
add name=up-voip parent=up-root packet-mark=up_voip limit-at=10M max-limit=100M priority=1
add name=up-std parent=up-root packet-mark=up_std limit-at=30M max-limit=100M priority=4 \
queue=pcq-std-up
add name=up-prem parent=up-root packet-mark=up_prem limit-at=50M max-limit=100M priority=3 \
queue=pcq-prem-up

What this achieves:

  • Each Standard subscriber is individually capped at 20/5 Mbps by PCQ
  • Each Premium subscriber is individually capped at 50/20 Mbps by PCQ
  • VoIP traffic is protected across all tiers with a guaranteed 20/10 Mbps reserve
  • During low-congestion periods, subscribers can burst above their plan rate up to the tier ceiling
  • HTB ensures Premium class gets limit-at=100M guaranteed even if Standard is saturated

# Live queue statistics
/queue tree print stats
# Watch counters update in real time
/queue tree print stats interval=1
# PCQ sub-queue count and rates
/queue type print detail where kind=pcq
# Reset counters for fresh measurement
/queue tree reset-counters-all

The queue is not matching any traffic:

  1. Check that mangle rules are generating the expected packet marks: /ip firewall mangle print stats
  2. Confirm FastTrack is not bypassing mangle — see FastTrack interaction
  3. Verify packet-mark in the queue entry exactly matches the mark name in mangle (case-sensitive)
  4. Check that the queue parent interface matches the traffic direction (upload vs download)

The queue tree root is attached to the wrong direction, or the parent chain allows traffic to bypass the queue. Verify:

  • Upload tree parent = WAN interface name (e.g., ether1-WAN)
  • Download tree parent = global
  • No duplicate queue entries with higher limits

Check that VoIP mangle rules appear before catch-all rules (mangle is evaluated top-to-bottom, first match wins), and that VoIP traffic is not being incorrectly tagged by P2P or bulk rules that run earlier.

  • Confirm pcq-classifier matches the direction: use dst-address for download, src-address for upload
  • Check for NAT — if multiple users share one external IP, the classifier sees one address instead of many. Apply PCQ on the LAN-side interface in that case, or use src-address,dst-address combined classifier

Shaping is working but bufferbloat is present inside the shaped class. Add CAKE as the leaf queue type on the bottleneck class — see CAKE as a leaf in an HTB tree.