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 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
# 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
# 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 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 (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.
Adding delay
# 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
# 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
# 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
# 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
# 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
| Parameter | Example | Description |
|---|---|---|
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-K | loss gemodel 1% 10% 70% 0% | Gilbert-Elliott bursty loss model |
corrupt PERCENT | corrupt 0.1% | Randomly flip bits in the packet payload |
duplicate PERCENT | duplicate 1% | Randomly duplicate packets |
reorder PERCENT [CORR%] | reorder 25% 50% | Fraction of packets sent immediately (rest delayed) — causes reordering |
rate RATE | rate 10mbit | Bandwidth cap; units: kbit, mbit, gbit, kbps, mbps |
limit PACKETS | limit 1000 | Maximum packets in the netem queue before tail-drop |
distribution TYPE | distribution normal | Jitter distribution: uniform (default), normal, pareto, paretonormal |
slot MIN MAX | slot 800us 1ms | Packetize delivery into time slots (simulates GSM/LTE scheduling) |
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
# 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
| Parameter | Example | Description |
|---|---|---|
rate RATE | rate 1mbit | Sustained output rate. Units: kbit, mbit, gbit, kbps, mbps, gbps |
burst SIZE | burst 32kbit | Maximum burst size in tokens (bytes or bit-units). Should be ≥ rate/HZ. Larger burst allows more short-term bursty traffic. |
latency TIME | latency 400ms | Maximum time a packet may wait in the queue. Determines queue depth = rate × latency. Packets arriving to a full queue are dropped. |
mtu SIZE | mtu 1500 | MTU of the interface. Used to compute the minimum burst size. |
peakrate RATE | peakrate 2mbit | Optional peak rate (limits burst transmission speed). Requires mtu. |
minburst SIZE | minburst 1540 | Minimum burst for peakrate bucket. Typically set to MTU. |
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 (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
# 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
# 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
# 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
# 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)
# 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
# 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
| Parameter | Example | Description |
|---|---|---|
rate RATE | rate 5mbit | Guaranteed minimum bandwidth for this class |
ceil RATE | ceil 20mbit | Maximum bandwidth (including borrowed). Defaults to rate if omitted. |
burst SIZE | burst 15k | Burst size for the rate bucket. Larger = more bursty at guaranteed rate. |
cburst SIZE | cburst 15k | Burst size for the ceil bucket. Controls burst when borrowing. |
prio N | prio 1 | Priority for borrowing (lower = higher priority). Range: 0–7. |
quantum N | quantum 1514 | Amount borrowed per round when borrowing. Defaults to rate/r2q. |
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 (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
# 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
| Parameter | Default | Description |
|---|---|---|
target TIME | 5ms | Minimum acceptable queue delay. CoDel starts dropping when delay exceeds this. |
interval TIME | 100ms | Window for measuring delay. Should be ~RTT of the bottleneck link. |
quantum BYTES | 1514 | Bytes dequeued per round per flow. Larger = less fairness, higher throughput. |
flows N | 1024 | Number of hash buckets for flow classification. |
limit PACKETS | 10240 | Hard queue limit before tail-drop. |
ecn | off | Enable ECN (Explicit Congestion Notification) instead of dropping. |
noecn | Disable ECN (explicit drop only). |
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
# 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
| Option | Description |
|---|---|
bandwidth RATE | Set the egress bandwidth cap. Without this, cake acts as pure AQM with no shaping. |
nat | Enable NAT-aware flow hashing. Looks up pre-NAT addresses for correct per-host fairness on a router. |
dual-srchost | Fair queuing by source IP (egress). Each host gets an equal share of bandwidth. |
dual-dsthost | Fair queuing by destination IP (ingress via ifb). Each host gets an equal download share. |
triple-isolate | Fair queuing by both host and flow (combines srchost + dsthost + flow isolation). |
diffserv4 | 4-tier DSCP-based prioritisation: Bulk, Best Effort, Video, Voice. |
diffserv8 | 8-tier DSCP prioritisation. |
besteffort | Single queue, no DSCP prioritisation (default). |
ack-filter | Suppress redundant TCP ACKs to improve efficiency on asymmetric links. |
overhead N | Account for per-packet overhead (e.g., PPPoE: 8, Ethernet: 18). |
mpu N | Minimum packet unit — rounds up small packets for overhead accounting. |
ingress | Tell cake this is on an ingress-redirected ifb device (adjusts internal logic). |
wash | Clear DSCP bits on egress to prevent leaking internal markings to the internet. |
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
# 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
# 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
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.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
# 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
# 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
# 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
| Protocol | Usage |
|---|---|
ip | IPv4 packets |
ipv6 | IPv6 packets |
all | All protocols (use with MAC/VLAN matching) |
802.1Q | VLAN-tagged frames |
arp | ARP packets |
Ready-to-use tc configurations for common scenarios.
Recipe 1: Simulate a bad WAN link (testing)
# 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
# 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)
# 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
# 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
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
/etc/network/if-up.d/ hook, or a systemd service unit that runs ExecStart after networking is up.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.