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_codel or fq on 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.

  • Networking
  • Linux
  • Performance