Linux Traffic Control (tc)
Bandwidth shaping, rate limiting, and WAN emulation with tc, netem, htb, tbf, and cake
Application (send/recv) │ ┌──────┴───────────────────────────────────────────────────┐ │ Linux Kernel │ │ │ │ Socket buffer → Egress qdisc → NIC driver → NIC (tx) │ │ │ │ NIC (rx) → NIC driver → Ingress qdisc → Socket buffer │ └──────────────────────────────────────────────────────────┘ │ tc operates at the qdisc layer: Egress (root qdisc) — full control: shaping, scheduling, delay, loss Ingress (ingress qdisc) — limited; use ifb redirect for full control Qdisc types: netem — WAN emulation (delay, loss, corruption, reorder) tbf — Token Bucket Filter: smooth rate limiting htb — Hierarchical Token Bucket: classes + borrowing fq_codel — Fair queuing + CoDel AQM (default on many distros) cake — Modern all-in-one (bandwidth + fairness + AQM)
tc basics
net

tc (traffic control) is the Linux userspace tool for configuring the kernel's packet scheduler. It manages qdiscs (queuing disciplines), classes, and filters on network interfaces.

tc is not persistentAll tc rules live in kernel memory and are lost on reboot or interface restart. Use startup scripts, ip-up hooks, or systemd units to reapply rules automatically.

Show current configuration

bash
# Show all qdiscs on all interfaces
tc qdisc show

# Show qdiscs on a specific interface
tc qdisc show dev eth0

# Show classes (for classful qdiscs like htb)
tc class show dev eth0

# Show filters
tc filter show dev eth0

# Show statistics (packet counts, drops, overlimits)
tc -s qdisc show dev eth0
tc -s class show dev eth0

General add / change / del / replace syntax

bash
# Add a qdisc
tc qdisc add dev <iface> root <qdisc-type> [params...]

# Modify an existing qdisc (must already exist)
tc qdisc change dev <iface> root <qdisc-type> [params...]

# Add or replace (atomic: remove old if present, add new)
tc qdisc replace dev <iface> root <qdisc-type> [params...]

# Delete the root qdisc (removes all children too)
tc qdisc del dev <iface> root

# Add a class under an htb root
tc class add dev <iface> parent <parent-handle> classid <handle> htb [params...]

# Add a filter
tc filter add dev <iface> protocol ip parent <handle> [match...] flowid <class>

Handle and classid notation

Handles use the format major:minor. The root qdisc is typically 1:0 (written as 1:). Classes are 1:1, 1:10, etc. Filters attach to a qdisc handle and direct packets to a classid.

tc -s for statsRun tc -s qdisc show dev eth0 after applying rules to check packet counts, bytes sent, dropped packets, and overlimit events. This is the quickest way to verify your rules are matching traffic.
netem
net

netem (Network Emulator) is a qdisc that adds controllable delay, jitter, packet loss, corruption, duplication, and reordering to outgoing packets. It is the standard tool for WAN emulation and chaos testing of networked applications.

netem is for testing onlyNever use netem for production traffic shaping. It is a testing and emulation tool. For production rate limiting use tbf, htb, or cake. netem has no fairness or AQM properties.

Adding delay

bash
# Fixed 100ms delay on all outgoing packets
tc qdisc add dev eth0 root netem delay 100ms

# 100ms delay with ±20ms uniform jitter
tc qdisc add dev eth0 root netem delay 100ms 20ms

# 100ms delay with ±20ms jitter, 25% correlation between successive packets
tc qdisc add dev eth0 root netem delay 100ms 20ms 25%

# Normal distribution jitter (more realistic than uniform)
tc qdisc add dev eth0 root netem delay 100ms 20ms distribution normal

Packet loss

bash
# Independent 1% random packet loss
tc qdisc add dev eth0 root netem loss 1%

# 1% loss with 25% correlation (bursty loss, Gilbert-Elliott model)
tc qdisc add dev eth0 root netem loss 1% 25%

# State-based loss: p% chance of entering loss state, r% chance of recovery
tc qdisc add dev eth0 root netem loss gemodel 1% 10% 70% 0%

Packet corruption, duplication, reordering

bash
# Corrupt 0.1% of packets (flip random bits)
tc qdisc add dev eth0 root netem corrupt 0.1%

# Duplicate 1% of packets
tc qdisc add dev eth0 root netem duplicate 1%

# Reorder: 10ms base delay, 25% of packets sent immediately (reordered),
# with 50% correlation between reordering decisions
tc qdisc add dev eth0 root netem delay 10ms reorder 25% 50%

Rate limiting via netem

bash
# Limit bandwidth to 10mbit (netem internal rate)
tc qdisc add dev eth0 root netem rate 10mbit

# Combine delay + loss + rate in one rule
tc qdisc add dev eth0 root netem \
  delay 100ms 20ms \
  loss 1% \
  rate 10mbit

Modifying an existing netem rule

bash
# Change parameters without removing (use 'change' not 'add')
tc qdisc change dev eth0 root netem delay 200ms loss 2%

# Remove all tc rules on the interface
tc qdisc del dev eth0 root

netem parameters reference

ParameterExampleDescription
delay TIME [JITTER [CORR%]]delay 100ms 20ms 25%Add fixed delay, optional ±jitter, optional inter-packet correlation
loss PERCENT [CORR%]loss 1% 25%Random independent or correlated packet loss
loss gemodel P R 1-H 1-Kloss gemodel 1% 10% 70% 0%Gilbert-Elliott bursty loss model
corrupt PERCENTcorrupt 0.1%Randomly flip bits in the packet payload
duplicate PERCENTduplicate 1%Randomly duplicate packets
reorder PERCENT [CORR%]reorder 25% 50%Fraction of packets sent immediately (rest delayed) — causes reordering
rate RATErate 10mbitBandwidth cap; units: kbit, mbit, gbit, kbps, mbps
limit PACKETSlimit 1000Maximum packets in the netem queue before tail-drop
distribution TYPEdistribution normalJitter distribution: uniform (default), normal, pareto, paretonormal
slot MIN MAXslot 800us 1msPacketize delivery into time slots (simulates GSM/LTE scheduling)
tbf
net

tbf (Token Bucket Filter) is a simple classless qdisc for smooth egress rate limiting. A token bucket is filled at the configured rate; each packet consumes tokens equal to its size. When no tokens are available, packets are delayed (up to latency ms) then dropped. tbf is ideal when you need a single smooth rate cap on an interface with no per-flow fairness requirements.

Basic usage

bash
# Limit egress to 1mbit with 32kbit burst, queue up to 400ms worth of packets
tc qdisc add dev eth0 root tbf \
  rate 1mbit \
  burst 32kbit \
  latency 400ms

# More conservative burst, short latency (tighter shaping)
tc qdisc add dev eth0 root tbf \
  rate 512kbit \
  burst 16kbit \
  latency 50ms

# Show stats to verify
tc -s qdisc show dev eth0

tbf parameters

ParameterExampleDescription
rate RATErate 1mbitSustained output rate. Units: kbit, mbit, gbit, kbps, mbps, gbps
burst SIZEburst 32kbitMaximum burst size in tokens (bytes or bit-units). Should be ≥ rate/HZ. Larger burst allows more short-term bursty traffic.
latency TIMElatency 400msMaximum time a packet may wait in the queue. Determines queue depth = rate × latency. Packets arriving to a full queue are dropped.
mtu SIZEmtu 1500MTU of the interface. Used to compute the minimum burst size.
peakrate RATEpeakrate 2mbitOptional peak rate (limits burst transmission speed). Requires mtu.
minburst SIZEminburst 1540Minimum burst for peakrate bucket. Typically set to MTU.
Choosing burst sizeThe burst must be at least rate / kernel_HZ. On a system with HZ=250, a 1mbit rate requires at least 500 bytes of burst. Too small a burst causes the kernel to reject the configuration. A good default is rate / 8 (in bytes) or simply 32kbit for most rates.
htb
net

htb (Hierarchical Token Bucket) is a classful qdisc that organises traffic into a tree of classes. Each class has a guaranteed rate (rate) and a ceiling (ceil). When a class is underusing its rate, spare tokens can be borrowed by child classes up to their ceiling. Filters classify packets into leaf classes; a default class catches unclassified traffic.

HTB class hierarchy

bash
# Structure we'll build:
#   1:0  root qdisc (htb)
#   1:1  root class (total bandwidth ceiling = 20mbit)
#   1:10 SSH class   — guaranteed 5mbit, ceil 20mbit
#   1:20 HTTP class  — guaranteed 10mbit, ceil 20mbit
#   1:30 default     — guaranteed 1mbit, ceil 20mbit

Step 1: Create the root qdisc

bash
# Add htb root qdisc; default 30 sends unclassified packets to class 1:30
tc qdisc add dev eth0 root handle 1: htb default 30

Step 2: Add the root class

bash
# Root class sets the total bandwidth envelope
tc class add dev eth0 parent 1: classid 1:1 htb \
  rate 20mbit \
  ceil 20mbit

Step 3: Add leaf classes

bash
# SSH class: guaranteed 5mbit, can borrow up to 20mbit when available
tc class add dev eth0 parent 1:1 classid 1:10 htb \
  rate 5mbit \
  ceil 20mbit \
  prio 1

# HTTP class: guaranteed 10mbit, can borrow up to 20mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb \
  rate 10mbit \
  ceil 20mbit \
  prio 2

# Default / best-effort class: guaranteed 1mbit, can borrow up to 20mbit
tc class add dev eth0 parent 1:1 classid 1:30 htb \
  rate 1mbit \
  ceil 20mbit \
  prio 3

Step 4: Attach leaf qdiscs (optional but recommended)

bash
# Attach fq_codel as the inner qdisc for each leaf class
# This provides per-flow fairness and low latency within each HTB class
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel
tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel
tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel

Step 5: Classify traffic with filters

bash
# Send SSH (dst port 22) to class 1:10
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 \
  match ip dport 22 0xffff \
  flowid 1:10

# Send HTTP (dst port 80) to class 1:20
tc filter add dev eth0 protocol ip parent 1: prio 2 u32 \
  match ip dport 80 0xffff \
  flowid 1:20

# Send HTTPS (dst port 443) to class 1:20
tc filter add dev eth0 protocol ip parent 1: prio 3 u32 \
  match ip dport 443 0xffff \
  flowid 1:20

# Everything else falls to default class 1:30 (set with 'default 30' above)

HTB class parameters

ParameterExampleDescription
rate RATErate 5mbitGuaranteed minimum bandwidth for this class
ceil RATEceil 20mbitMaximum bandwidth (including borrowed). Defaults to rate if omitted.
burst SIZEburst 15kBurst size for the rate bucket. Larger = more bursty at guaranteed rate.
cburst SIZEcburst 15kBurst size for the ceil bucket. Controls burst when borrowing.
prio Nprio 1Priority for borrowing (lower = higher priority). Range: 0–7.
quantum Nquantum 1514Amount borrowed per round when borrowing. Defaults to rate/r2q.
Borrowing rulesA class can only borrow bandwidth from its parent, and only up to its ceil. Higher-priority classes (prio 1) get to borrow before lower-priority ones. If a parent class has spare capacity, child classes with lower prio still get to use it once higher-prio classes are satisfied.
fq_codel
net

fq_codel (Fair Queuing + Controlled Delay) combines stochastic flow-based fair queuing with the CoDel AQM (Active Queue Management) algorithm. It provides per-flow fairness, low latency under load, and automatic bufferbloat mitigation. It is the default qdisc on many modern Linux distributions (e.g., Ubuntu 20.04+).

Basic usage

bash
# Apply fq_codel as the root qdisc (usually already the default)
tc qdisc add dev eth0 root fq_codel

# With custom parameters
tc qdisc add dev eth0 root fq_codel \
  target 5ms \
  interval 100ms \
  quantum 1514 \
  flows 1024

# Check current default qdisc
sysctl net.core.default_qdisc

Key parameters

ParameterDefaultDescription
target TIME5msMinimum acceptable queue delay. CoDel starts dropping when delay exceeds this.
interval TIME100msWindow for measuring delay. Should be ~RTT of the bottleneck link.
quantum BYTES1514Bytes dequeued per round per flow. Larger = less fairness, higher throughput.
flows N1024Number of hash buckets for flow classification.
limit PACKETS10240Hard queue limit before tail-drop.
ecnoffEnable ECN (Explicit Congestion Notification) instead of dropping.
noecnDisable ECN (explicit drop only).
When to use fq_codelfq_codel is a good general-purpose default for most Linux hosts. It prevents bufferbloat without requiring explicit rate configuration. For routers and gateways, pair it with a bandwidth-limiting qdisc (tbf or cake) to get both rate control and AQM.
cake
net

cake (Common Applications Kept Enhanced) is a modern all-in-one qdisc that combines bandwidth shaping, per-flow fair queuing, CoDel-based AQM, and traffic classification into a single configuration. It is the recommended replacement for tbf + fq_codel combinations and is particularly well-suited for home routers and access links.

Basic usage

bash
# Simple rate-limited cake (replaces tbf + fq_codel)
tc qdisc add dev eth0 root cake \
  bandwidth 100mbit

# Cake with NAT-aware flow hashing (correct for home routers)
tc qdisc add dev eth0 root cake \
  bandwidth 100mbit \
  nat

# Egress shaping with per-host fairness (each src IP gets equal share)
tc qdisc add dev eth0 root cake \
  bandwidth 100mbit \
  dual-srchost

# Full-featured ISP uplink setup
tc qdisc add dev eth0 root cake \
  bandwidth 95mbit \
  nat \
  dual-srchost \
  diffserv4 \
  ack-filter

Key cake options

OptionDescription
bandwidth RATESet the egress bandwidth cap. Without this, cake acts as pure AQM with no shaping.
natEnable NAT-aware flow hashing. Looks up pre-NAT addresses for correct per-host fairness on a router.
dual-srchostFair queuing by source IP (egress). Each host gets an equal share of bandwidth.
dual-dsthostFair queuing by destination IP (ingress via ifb). Each host gets an equal download share.
triple-isolateFair queuing by both host and flow (combines srchost + dsthost + flow isolation).
diffserv44-tier DSCP-based prioritisation: Bulk, Best Effort, Video, Voice.
diffserv88-tier DSCP prioritisation.
besteffortSingle queue, no DSCP prioritisation (default).
ack-filterSuppress redundant TCP ACKs to improve efficiency on asymmetric links.
overhead NAccount for per-packet overhead (e.g., PPPoE: 8, Ethernet: 18).
mpu NMinimum packet unit — rounds up small packets for overhead accounting.
ingressTell cake this is on an ingress-redirected ifb device (adjusts internal logic).
washClear DSCP bits on egress to prevent leaking internal markings to the internet.
cake vs tbf + fq_codelPrefer cake when you want a single self-contained qdisc. It handles shaping, fairness, and AQM together with NAT awareness. Use tbf + fq_codel or htb + fq_codel when you need fine-grained class hierarchies or compatibility with older kernels (cake requires 4.19+).
Ingress traffic (ifb trick)
net

The Linux ingress qdisc is limited — it only supports filtering and policing, not full shaping. The standard workaround is to use an Intermediate Functional Block (ifb) virtual device: redirect all incoming traffic from the real interface to ifb0, then apply a full egress qdisc on ifb0. From the kernel's perspective it becomes egress traffic and all qdiscs are available.

Setup: redirect ingress to ifb

bash
# 1. Load the ifb kernel module
modprobe ifb numifbs=1

# 2. Bring up ifb0
ip link set dev ifb0 up

# 3. Add an ingress qdisc on the real interface
tc qdisc add dev eth0 handle ffff: ingress

# 4. Redirect all ingress traffic from eth0 to ifb0
tc filter add dev eth0 parent ffff: protocol ip u32 \
  match u32 0 0 \
  action mirred egress redirect dev ifb0

# 5. Apply your shaping qdisc on ifb0 (now controls ingress on eth0)
tc qdisc add dev ifb0 root cake \
  bandwidth 50mbit \
  ingress \
  dual-dsthost

Teardown

bash
# Remove ingress qdisc from real interface (removes filter too)
tc qdisc del dev eth0 handle ffff: ingress

# Remove shaping from ifb0
tc qdisc del dev ifb0 root

# Take down ifb0
ip link set dev ifb0 down
Alternative: tc police (ingress policing)For simple ingress rate policing (drop-only, no shaping/delay), you can use tc filter … action police rate 50mbit burst 100k drop directly on the ffff: ingress qdisc without ifb. This is simpler but discards excess packets immediately rather than smoothing them.
tc filters
net

Filters classify packets into HTB classes (or redirect them via actions). The three most common filter types are u32 (arbitrary byte matching), flower (key-value flow matching), and bpf (eBPF programs for arbitrary classification). Filters are attached to a qdisc handle and evaluated in priority order.

u32 filters — arbitrary field matching

bash
# Match destination port 80 (TCP/UDP), send to HTB class 1:20
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 \
  match ip dport 80 0xffff \
  flowid 1:20

# Match source IP 192.168.1.100
tc filter add dev eth0 protocol ip parent 1: prio 2 u32 \
  match ip src 192.168.1.100/32 \
  flowid 1:30

# Match destination subnet 10.0.0.0/8
tc filter add dev eth0 protocol ip parent 1: prio 3 u32 \
  match ip dst 10.0.0.0/8 \
  flowid 1:10

# Match DSCP value 0x28 (AF31) — match on the TOS byte
tc filter add dev eth0 protocol ip parent 1: prio 4 u32 \
  match ip tos 0x28 0xfc \
  flowid 1:10

flower filters — structured key-value matching

bash
# Match TCP traffic to port 443 (HTTPS)
tc filter add dev eth0 protocol ip parent 1: prio 1 flower \
  ip_proto tcp \
  dst_port 443 \
  action goto chain 0

# Match by source MAC address
tc filter add dev eth0 protocol all parent 1: prio 1 flower \
  src_mac aa:bb:cc:dd:ee:ff \
  flowid 1:10

# Match by VLAN ID
tc filter add dev eth0 protocol 802.1Q parent 1: prio 1 flower \
  vlan_id 100 \
  flowid 1:20

bpf filters — eBPF classification

bash
# Attach a pre-compiled eBPF classifier program
tc filter add dev eth0 parent 1: bpf \
  obj classifier.o sec classifier \
  direct-action

# List all filters on eth0
tc filter show dev eth0

# Delete all filters on parent 1:
tc filter del dev eth0 parent 1:

Filter protocol values

ProtocolUsage
ipIPv4 packets
ipv6IPv6 packets
allAll protocols (use with MAC/VLAN matching)
802.1QVLAN-tagged frames
arpARP packets
Practical recipes
reference

Ready-to-use tc configurations for common scenarios.

Recipe 1: Simulate a bad WAN link (testing)

bash
# Simulate 100ms RTT delay, 1% packet loss, and 10mbit rate cap
# Typical for testing resilience of distributed systems / microservices
tc qdisc add dev eth0 root netem \
  delay 100ms 10ms \
  loss 1% 25% \
  rate 10mbit

# To undo:
tc qdisc del dev eth0 root

Recipe 2: Rate-limit a specific source IP to 1mbit

bash
# Create HTB root with default class (1:99 = unlimited)
tc qdisc add dev eth0 root handle 1: htb default 99
tc class add dev eth0 parent 1: classid 1:1 htb rate 1gbit ceil 1gbit

# Unlimited best-effort class for everyone else
tc class add dev eth0 parent 1:1 classid 1:99 htb rate 900mbit ceil 1gbit

# Limited class for 192.168.1.50
tc class add dev eth0 parent 1:1 classid 1:50 htb rate 1mbit ceil 1mbit

# Filter: src 192.168.1.50 → class 1:50
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 \
  match ip src 192.168.1.50/32 \
  flowid 1:50

Recipe 3: HTB with 3 priority classes (high/medium/low)

bash
# Root HTB, default to low-priority class
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit ceil 100mbit

# High priority: VoIP / interactive — guaranteed 20mbit, can burst to 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb \
  rate 20mbit ceil 100mbit prio 1

# Medium priority: web browsing — guaranteed 50mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb \
  rate 50mbit ceil 100mbit prio 2

# Low priority: bulk transfers / backups — guaranteed 10mbit
tc class add dev eth0 parent 1:1 classid 1:30 htb \
  rate 10mbit ceil 100mbit prio 3

# Attach fq_codel leaf qdiscs for bufferbloat mitigation
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel
tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel
tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel

# Classify DSCP EF (VoIP) to high priority
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 \
  match ip tos 0xb8 0xfc \
  flowid 1:10

# Classify SSH to high priority
tc filter add dev eth0 protocol ip parent 1: prio 2 u32 \
  match ip dport 22 0xffff \
  flowid 1:10

Recipe 4: Reset all tc rules on an interface

bash
# Delete root qdisc (this cascades and removes all classes and filters)
tc qdisc del dev eth0 root 2>/dev/null

# Delete ingress qdisc (separate from root)
tc qdisc del dev eth0 handle ffff: ingress 2>/dev/null

# Reset ifb0 too if used for ingress shaping
tc qdisc del dev ifb0 root 2>/dev/null
ip link set dev ifb0 down 2>/dev/null

# Verify the interface is back to the default pfifo_fast or fq_codel
tc qdisc show dev eth0
Reference cheat sheet
reference

Show commands

tc qdisc show dev eth0
tc -s qdisc show dev eth0
tc class show dev eth0
tc filter show dev eth0
tc -s -d qdisc show dev eth0

netem one-liners

tc qdisc add dev eth0 root netem delay 100ms
tc qdisc add dev eth0 root netem delay 100ms 20ms 25%
tc qdisc add dev eth0 root netem loss 1%
tc qdisc add dev eth0 root netem corrupt 0.1%
tc qdisc add dev eth0 root netem duplicate 1%

tbf one-liners

tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 400ms
tc qdisc add dev eth0 root tbf rate 512kbit burst 16kbit latency 50ms
tc qdisc change dev eth0 root tbf rate 2mbit burst 32kbit latency 400ms

HTB skeleton

tc qdisc add dev eth0 root handle 1: htb default 99
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 10mbit ceil 100mbit prio 1
tc class add dev eth0 parent 1:1 classid 1:99 htb rate 90mbit ceil 100mbit prio 2
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip dport 22 0xffff flowid 1:10

cake one-liners

tc qdisc add dev eth0 root cake bandwidth 100mbit
tc qdisc add dev eth0 root cake bandwidth 95mbit nat dual-srchost diffserv4
tc qdisc add dev ifb0 root cake bandwidth 50mbit ingress dual-dsthost

Reset interface

tc qdisc del dev eth0 root 2>/dev/null
tc qdisc del dev eth0 handle ffff: ingress 2>/dev/null
tc qdisc del dev ifb0 root 2>/dev/null
ip link set dev ifb0 down 2>/dev/null

Changes are not persistentAll tc rules are lost on reboot or when the network interface is restarted. To make rules persistent, add them to a startup script, a /etc/network/if-up.d/ hook, or a systemd service unit that runs ExecStart after networking is up.
Useful tc flagsRun tc -s to add statistics (bytes, packets, drops, overlimits) to any show command. Add -d for more detail (options and internal state). Add -p to pretty-print filter selectors.