Originally published at https://blog.pathvector.dev/protocol-lab-qos-28/ — part of the free Protocol Lab series.
This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.
In Lab 08, tc netem added delay and loss to a link. This lab uses tc for the other classic job: traffic shaping — deliberately capping how fast a link may send. You attach a token-bucket queueing discipline (tbf) to the sender's egress and measure the effect with iperf3.
Reading guide: rfc-notes/qos-traffic-shaping.md
Prerequisite: TCP Lab 08: Loss, Retransmission, and the Window
Expected time: 40–55 minutes.
The Goal
- First,
iperf3runs at the veth's native speed (tens of Gbit/s inside a container), - then a
tc tbfshaper caps the client's egress at 10 Mbit/s, -
iperf3now measures ~10 Mbit/s — the token bucket meters the traffic to the configured rate.
By the end, you should be able to explain this table:
| State | iperf3 throughput |
|---|---|
| no shaping | tens of Gbit/s (native veth) |
tbf rate 10mbit |
~10 Mbit/s (metered to the rate) |
What You Will Learn
- What traffic shaping is, and how it differs from netem's delay/loss (Lab 08).
- What a queueing discipline (qdisc) is in Linux
tc. - How a token bucket (
tbf) meters traffic: tokens accumulate at the rate, and a packet needs tokens to leave. - Why
burstandlatencymatter (and why a too-small burst throttles almost to zero). - Where shaping is used (rate plans, fair-sharing, protecting a slow uplink).
This lab does not cover:
- Classful shaping (HTB) with multiple classes and priorities.
- Marking/policing (DiffServ DSCP) or ingress shaping.
- Fair queueing (fq, fq_codel) and bufferbloat in depth.
Where to Read in the RFCs
Traffic shaping is a Linux-implementation (tc) topic, so there is no single defining RFC. These references cover the related concepts:
| Reference | What to focus on |
|---|---|
tc-tbf(8) man page |
The token bucket filter's parameters (rate, burst, latency) |
| RFC 2475 | The DiffServ architecture (where shaping/policing fit in) |
| RFC 2697 | Single Rate Three Color Marker (the general form of a token bucket) |
| RFC 5737 | Confirming the addresses used here are documentation-only |
The Big Picture
Two nodes: a client and a server. The server runs iperf3 -s.
client (10.0.0.1) ------ eth1/eth1 ------ server (10.0.0.2, iperf3 -s)
tc tbf on egress measure throughput with iperf3
Attach a token bucket to the client's egress (eth1), and everything leaving through it is metered down to the configured rate.
flowchart LR
app["client app<br/>(iperf3)"] --> q["tbf qdisc<br/>rate 10mbit"]
q --> link["eth1 (native speed)"]
link --> S["server"]
note["tokens fill at 10 Mbit/s.<br/>a packet leaves only when<br/>enough tokens exist."]
Note:
10.0.0.0/24is a local, closed range, so nothing in this lab touches the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
Images used:
-
nicolaka/netshoot:latest— bundles bothtcandiperf3.
No additional images are required.
Running the Lab
The quick path, which deploys, verifies, and tears down for you:
./scripts/labctl.sh run qos-28
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/qos-28
2. Deploy and start the iperf3 server
sudo containerlab deploy -t qos-28.clab.yml
docker exec -d clab-qos-28-server iperf3 -s
3. Measure the unshaped baseline
docker exec clab-qos-28-client iperf3 -c 10.0.0.2 -t 4
This is a veth pair, so expect tens of Gbit/s (the exact figure depends on your machine).
4. Cap it at 10 Mbit/s with a token bucket
docker exec clab-qos-28-client tc qdisc add dev eth1 root tbf rate 10mbit burst 32kb latency 100ms
docker exec clab-qos-28-client tc -s qdisc show dev eth1
docker exec clab-qos-28-client iperf3 -c 10.0.0.2 -t 4
Throughput drops to roughly 10 Mbit/s.
Warning:
burstis in bytes:32kb= 32 kilobytes. If you accidentally specify it in bits —32kbit, which is only 4 KB — the shaper throttles the link almost to zero.
5. Remove the shaper and restore full speed
docker exec clab-qos-28-client tc qdisc del dev eth1 root
docker exec clab-qos-28-client iperf3 -c 10.0.0.2 -t 4 # fast again
Expected Output
- Unshaped
iperf3: tens of Gbit/s (fast, environment-dependent). - With
tbf rate 10mbit: about 10 Mbit/s. - After deleting the qdisc: fast again.
Why It Works
QoS (Quality of Service) work with tc comes in two flavors: "add delay and loss" (Lab 08's netem) and "cap the speed" (shaping). This lab is the latter. Shaping means putting an artificial ceiling on how fast a link may send.
-
qdisc (queueing discipline). Linux hangs a qdisc on each interface's egress; it decides how packets are sent out — ordering, timing, and drops.
tcis the tool that configures it. The default is a simple FIFO-style qdisc. -
Token bucket (tbf). "Tokens" accumulate in a bucket at the configured rate (here 10 Mbit/s). Sending a packet costs tokens. If tokens are available, the packet leaves immediately; if not, it waits. Over the long run traffic is held to the rate; momentarily, it may burst up to however many tokens have been saved. That is how the average speed is metered to the rate.
-
burst is the bucket size — how many tokens it can hold, i.e. how much can leave in one go. Too small, and only a trickle can be sent at each instant, dragging effective throughput far below the rate (in this lab,
32kbit= 4 KB dropped it to about 0.5 Mbit/s). So you need a burst, in bytes, sized to match the rate. - latency is the maximum time a packet may wait in the queue. Exceed it and the packet is dropped — effectively a cap on queue depth.
-
burst is the bucket size — how many tokens it can hold, i.e. how much can leave in one go. Too small, and only a trickle can be sent at each instant, dragging effective throughput far below the rate (in this lab,
-
Shaping vs. policing. Shaping delays traffic to smooth it (queue packets and pace them to the rate). Policing drops or marks anything over the limit immediately.
tbfis shaping. - Where it's used. Enforcing contracted bandwidth (ISP rate plans), fair-sharing so one user can't monopolize a line, and pacing a sender to protect a slow upstream link (a bufferbloat countermeasure).
The key insight: control the egress with a qdisc, and let the token bucket meter the average speed to the configured rate. Alongside netem (delay/loss), this is tc's other major job.
Common Pitfalls
-
Confusing shaping with netem. netem adds delay and loss; shaping caps speed. Both live in
tc, but they serve different purposes. -
The burst unit.
tc's burst is in bytes:32kb= 32 KB, while32kbit= 4 KB (far too small). Size it to match the rate. -
It only works on egress.
tbfshapes transmission (egress). Shaping ingress requires a different mechanism (e.g.ifb). - veth is very fast. Container veth pairs push tens of Gbit/s, so the unshaped baseline varies a lot by environment.
-
Shaping vs. policing. Queue-and-smooth (shaping) or drop-the-excess (policing)?
tbfis the former. -
Measurement noise.
iperf3results wobble a bit. Judge success by whether the result lands near the configured rate.
Cleanup
sudo containerlab destroy -t qos-28.clab.yml --cleanup
If you used labctl.sh run qos-28, the script runs destroy for you at the end.
Check Your Understanding
- What is traffic shaping? How does it differ from Lab 08's netem (delay/loss)?
- What is a qdisc in Linux? What does
tcconfigure? - How does a token bucket (
tbf) meter the average speed to the rate? - What does
burstrepresent? What happens when it's too small? What unit is it in? - What's the difference between shaping and policing? Which one is
tbf? - Name two situations where shaping is used.
References
- tc-tbf(8) manual page
- RFC 2475: An Architecture for Differentiated Services
- RFC 2697: A Single Rate Three Color Marker
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
Verified Run Log (2026-07-07)
This lab has been confirmed reproducible on real hardware.
Environment:
- Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
- Docker 29.1.3
- containerlab 0.77.0
- client / server:
nicolaka/netshoot:latest(tc,iperf3bundled)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run qos-28 performed deploy → verify → destroy, and verification.json returned "status": "verified".
Native speed → 10 Mbit/s under the token bucket
[protocol-lab][qos-28] baseline: 56551 Mbit/s
[protocol-lab][qos-28] + tc qdisc add dev eth1 root tbf rate 10mbit burst 32kb latency 100ms
[protocol-lab][qos-28] shaped: 9 Mbit/s
Before shaping, the veth ran at its native 56551 Mbit/s (about 56 Gbit/s). After attaching tbf rate 10mbit to the client's egress, iperf3 throughput fell to 9 Mbit/s — right around the configured rate. The token bucket is metering the average speed to the rate.
(The burst unit really is the crux: with 32kb = 32 KB the link settles at about 10 Mbit/s, but a too-small 32kbit = 4 KB over-throttles it to roughly 0.5 Mbit/s.)
Cleanup
containerlab destroy -t qos-28.clab.yml --cleanup
That's traffic shaping: hang a token-bucket qdisc on the egress, and the link's average speed obediently settles at whatever rate you configured — no cooperation from the application required.
Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.
Next up, we'll go beyond a single rate cap and look at classful shaping — carving one link into multiple prioritized classes with HTB.
Top comments (0)