Sometimes you need to control traffic rather than just observe it: cap backup uploads so they don’t saturate the link, keep a busy download service from starving SSH, guarantee bandwidth to an important service, or simulate a slow, lossy network to test an application. Linux does all of this with tc (traffic control), part of the iproute2 package.
Concepts in 60 seconds
- qdisc (queuing discipline): decides how packets leaving an interface are queued and sent. Every interface has one (
fq_codelorfqon modern systems). - Classful qdiscs (like HTB) split traffic into classes with their own rates.
- Filters decide which packets go into which class.
- Egress vs ingress: tc shapes outgoing traffic well. Incoming traffic can only be policed (dropped above a rate), or redirected to a virtual interface (IFB) for shaping.
tc qdisc show dev eth0
tc -s qdisc show dev eth0 # with statistics
Limit all outgoing traffic on an interface
Token Bucket Filter, the simplest cap:
sudo tc qdisc replace dev eth0 root tbf rate 200mbit burst 256kb latency 50ms
Remove it:
sudo tc qdisc del dev eth0 root
Warning: shaping the main interface affects everything, SSH included. Test with conservative values, and keep a console session ready. See Linux rescue mode.
Different limits per destination or service: HTB
Example: cap traffic to a backup server at 50 Mbit/s, while everything else may use up to the full link:
DEV=eth0
sudo tc qdisc replace dev $DEV root handle 1: htb default 20
sudo tc class add dev $DEV parent 1: classid 1:1 htb rate 1gbit
# class 10: backups, 50 Mbit guaranteed and capped
sudo tc class add dev $DEV parent 1:1 classid 1:10 htb rate 50mbit ceil 50mbit
# class 20: everything else, can use the full link
sudo tc class add dev $DEV parent 1:1 classid 1:20 htb rate 900mbit ceil 1gbit
# fair queuing inside each class
sudo tc qdisc add dev $DEV parent 1:10 fq_codel
sudo tc qdisc add dev $DEV parent 1:20 fq_codel
# filter: traffic to the backup server → class 10
sudo tc filter add dev $DEV protocol ip parent 1: prio 1 u32 match ip dst 198.51.100.20/32 flowid 1:10
Match on destination port instead (e.g. outgoing rsync over SSH to port 22) with match ip dport 22 0xffff.
Check counters:
tc -s class show dev eth0
Simpler alternatives for single jobs: many tools have built-in limits, such as rsync --bwlimit=20M, curl --limit-rate 5M, scp -l 40000 (Kbit/s) and wget --limit-rate. Use them first. See rsync.
Fair queuing: fq_codel and cake
Even without hard limits, a good qdisc reduces latency under load (bufferbloat). One bulk transfer then can’t make interactive traffic like SSH or game packets wait behind a huge queue:
sudo tc qdisc replace dev eth0 root fq_codel
CAKE combines shaping, fairness and bufferbloat control in one qdisc, and is excellent on routers and gateways:
sudo tc qdisc replace dev eth0 root cake bandwidth 900mbit
For servers using TCP BBR, fq is the recommended root qdisc. See TCP BBR.
Limit incoming traffic (policing)
sudo tc qdisc add dev eth0 handle ffff: ingress
sudo tc filter add dev eth0 parent ffff: protocol ip u32 match ip src 0.0.0.0/0 \
police rate 100mbit burst 1m drop flowid :1
This drops packets above the rate, and TCP senders slow down in response. For true ingress shaping, redirect to an ifb device and apply HTB there.
Simulate bad networks with netem
Test how your app behaves for distant or mobile users. Use this on test machines, or a dedicated interface:
# add 150 ms delay with 20 ms jitter
sudo tc qdisc add dev eth0 root netem delay 150ms 20ms
# add 2% packet loss
sudo tc qdisc change dev eth0 root netem delay 150ms 20ms loss 2%
# limit rate as well
sudo tc qdisc change dev eth0 root netem delay 100ms rate 5mbit
# remove
sudo tc qdisc del dev eth0 root
Combine it with the browser’s dev tools or load tests to see real-world behaviour. See latency vs bandwidth and packet loss troubleshooting.
Making rules persistent
tc rules vanish at reboot. Put them in a script run by a systemd oneshot unit after the network is up, or use your network manager’s hooks (networkd-dispatcher, /etc/network/if-up.d/). See systemd unit files.
Easier front-ends
- wondershaper: a simple upload and download limit per interface.
- FireQOS (from the FireHOL suite) and tcconfig: friendlier syntax for tc.
- Per-container limits: Docker doesn’t shape bandwidth natively, but you can use tc on the container’s veth interface, or Kubernetes bandwidth annotations.
Quick reference
| Task | Command |
|---|---|
| Show qdiscs | tc -s qdisc show dev eth0 |
| Simple cap | tc qdisc replace dev eth0 root tbf rate 200mbit burst 256kb latency 50ms |
| Fair queuing | tc qdisc replace dev eth0 root fq_codel |
| Per-destination limits | HTB classes + u32 filters |
| Simulate latency and loss | tc qdisc add dev eth0 root netem delay 150ms loss 2% |
| Remove | tc qdisc del dev eth0 root |
Related: vnStat bandwidth monitoring and iftop and nethogs.