Traffic Shaping with tc: Fixing Bufferbloat and Limiting Bandwidth

Traffic Shaping with tc: Fixing Bufferbloat and Limiting Bandwidth

tc controls the queueing disciplines the kernel uses for outgoing packets. Most people encounter it wanting to limit bandwidth, and the more valuable thing it does is fix latency.

Bufferbloat, the actual problem

Start an upload and watch your ping time.

# In one terminal
ping -i 0.2 1.1.1.1

# In another
# start a large upload, then watch the ping times

On many connections, latency goes from 20ms to several seconds. Video calls stutter, SSH becomes unusable, and pages stop loading, all while the upload reports full speed.

The cause is oversized buffers in modems and provider equipment. Rather than dropping packets when a link saturates, which is the signal TCP uses to slow down, the equipment queues them. A packet that should have been dropped instead waits in a queue for two seconds.

This is bufferbloat, and it is why a saturated connection feels broken rather than merely busy.

CAKE

The modern answer, and it is genuinely close to one line.

sudo apt install iproute2

# Replace the default queue on your interface
sudo tc qdisc replace dev eth0 root cake bandwidth 90mbit

CAKE combines shaping, fair queueing between flows, and active queue management. Its design goal was explicitly to have few knobs.

Set the bandwidth below your real capacity, typically 90 to 95 percent. This is the critical detail: shaping only works if the queue forms on your machine where you control it. Set it at or above your line rate and the queue forms in your modem instead, and you have accomplished nothing.

Measure your actual throughput first, then subtract.

# For an asymmetric connection, shape upload on the WAN interface
sudo tc qdisc replace dev eth0 root cake bandwidth 18mbit

Upload is usually the direction that matters on a consumer connection, because it is the smaller number and the one that saturates.

fq_codel

If CAKE is unavailable, fq_codel is the predecessor and is already the default on many distributions.

tc qdisc show dev eth0
sudo tc qdisc replace dev eth0 root fq_codel

It provides fair queueing and queue management without shaping, so it helps when your machine is the bottleneck and not when the bottleneck is downstream.

Rate limiting a specific thing

For a hard cap rather than latency management:

# Simple rate limit
sudo tc qdisc add dev eth0 root tbf rate 10mbit burst 32kbit latency 400ms

# Remove it
sudo tc qdisc del dev eth0 root

For per-service limits, cgroups through systemd are usually easier to reason about than tc class hierarchies, and they apply to a unit rather than to matched traffic.

Simulating bad networks

An underappreciated use: making your connection deliberately worse to test software.

# Add 200ms of latency
sudo tc qdisc add dev eth0 root netem delay 200ms

# Latency with jitter
sudo tc qdisc add dev eth0 root netem delay 200ms 50ms distribution normal

# 5% packet loss
sudo tc qdisc add dev eth0 root netem loss 5%

# Remove
sudo tc qdisc del dev eth0 root

Testing an application against a simulated satellite link takes one command and finds timeout bugs that never appear on a fast local network.

Apply this to a container or VM interface rather than your main one, or you will be shaping your own SSH session.

Downloads

Queueing happens on egress. tc genuinely shapes what you send and can only police what you receive, by dropping packets that have already consumed your downstream capacity.

That still works, because TCP senders back off when packets are dropped. It is less efficient than shaping upload, and it is the best available at the receiving end.

The better place to shape download is the router, which is why routers running OpenWrt with CAKE handle this far better than any host-side configuration.

Persisting

tc rules are runtime state and vanish on reboot or when the interface goes down.

# /etc/systemd/system/tc-shaping.service
[Unit]
Description=Apply traffic shaping
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/tc qdisc replace dev eth0 root cake bandwidth 18mbit
ExecStop=/sbin/tc qdisc del dev eth0 root

[Install]
WantedBy=multi-user.target

Our systemd unit builder generates the file.

Checking it worked

tc -s qdisc show dev eth0     # statistics, including drops

The real test is the one you started with: saturate the link and watch ping times. If they stay near idle latency, the shaping is working.

Frequently Asked Questions

What is bufferbloat?

Excessive buffering in network equipment that turns a saturated link into a high-latency one. Packets queue in oversized buffers instead of being dropped, so a large upload can push ping times from twenty milliseconds to several seconds while throughput looks fine.

Can tc limit incoming traffic?

Only indirectly. Queueing happens on egress, so tc genuinely shapes what you send. Incoming traffic can be policed by dropping packets after they have already used your downstream capacity, which works for TCP because the sender backs off.

What is CAKE and why is it recommended?

A queueing discipline that combines shaping, fair queueing, and active queue management with very few knobs. Setting a bandwidth limit slightly below your real capacity is usually the entire configuration, and it handles the rest.

Why should I set the limit below my actual line speed?

Because shaping only works if the queue forms on your machine rather than in your modem or your provider equipment. Setting the limit at ninety to ninety-five percent of real capacity keeps control of the queue where you can manage it.

Do tc rules survive a reboot?

No. They are runtime state and disappear on restart or when the interface goes down. Persist them through a systemd unit, a NetworkManager dispatcher script, or your distribution network configuration.

Is tc still necessary with modern hardware?

For bufferbloat on an asymmetric consumer connection, frequently yes, because the problem lives in equipment you do not control. Many routers now ship CAKE or fq_codel built in, which is the better place to do it if yours supports it.