Quality of Service (QoS) with the Linux traffic control subsystem (tc) gives you precise control over bandwidth allocation, latency, and scheduling for network interfaces on RHEL 8. Using the Hierarchical Token Bucket (HTB) queuing discipline you can guarantee minimum bandwidth to critical services, cap bandwidth for bulk transfers, and prioritize interactive traffic over background workloads. This tutorial walks through inspecting the current qdisc, building an HTB hierarchy with multiple traffic classes, attaching classifier filters, and validating the configuration with iperf3. All changes made with tc are transient by default; persistence strategies using NetworkManager dispatcher scripts are also covered.
Prerequisites
- RHEL 8 server with root or sudo access and at least one network interface (examples use
ens3— substitute your own) iproutepackage installed (providestc):dnf install -y iprouteiperf3for bandwidth testing:dnf install -y iperf3- A second host or VM on the same network to act as the iperf3 client/server
- Basic understanding of networking concepts (IP addresses, ports, bandwidth)
Step 1 — Inspect the Current Queuing Discipline
Before making any changes, examine what queuing disciplines are already attached to your interface:
# Show all qdiscs on all interfaces
tc qdisc show
# Show qdiscs on a specific interface
tc qdisc show dev ens3
# Show existing classes (empty if no HTB has been configured)
tc class show dev ens3
# Show existing filters
tc filter show dev ens3
# Display current interface link speed and MTU
ip link show ens3
ethtool ens3 | grep Speed
A freshly configured interface typically shows only the default pfifo_fast or fq_codel qdisc with no classes or filters.
Step 2 — Add an HTB Root Qdisc
Replace the default qdisc with an HTB root qdisc. Traffic that does not match any filter falls into the default class 1:30:
# Remove any existing root qdisc first (ignore error if none exists)
tc qdisc del dev ens3 root 2>/dev/null || true
# Add HTB as the root qdisc; unmatched traffic goes to class 1:30
tc qdisc add dev ens3 root handle 1: htb default 30
# Verify
tc qdisc show dev ens3
Step 3 — Create HTB Traffic Classes
Build a two-level class hierarchy. The root class 1:1 caps total throughput at 100 Mbit/s; child classes share that bandwidth according to their rates and ceil values. Class 1:10 is for high-priority traffic, 1:20 for medium, and 1:30 is the default catch-all:
# Root class — total available bandwidth ceiling
tc class add dev ens3 parent 1: classid 1:1 htb rate 100mbit
# High-priority class: guaranteed 50 Mbit/s, can burst to 100 Mbit/s
tc class add dev ens3 parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit burst 15k prio 1
# Medium-priority class: guaranteed 20 Mbit/s, can burst to 50 Mbit/s
tc class add dev ens3 parent 1:1 classid 1:20 htb rate 20mbit ceil 50mbit burst 15k prio 2
# Default class: guaranteed 10 Mbit/s for all other traffic
tc class add dev ens3 parent 1:1 classid 1:30 htb rate 10mbit ceil 100mbit burst 15k prio 3
# Add SFQ (Stochastic Fair Queuing) leaf qdiscs to prevent starvation within each class
tc qdisc add dev ens3 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev ens3 parent 1:20 handle 20: sfq perturb 10
tc qdisc add dev ens3 parent 1:30 handle 30: sfq perturb 10
# Show the class hierarchy
tc class show dev ens3
Step 4 — Add Classifier Filters
Filters match packets and direct them to specific classes. The following examples use the u32 classifier to match by destination port and IP prefix:
# Direct HTTPS traffic (dport 443) to high-priority class 1:10
tc filter add dev ens3 protocol ip parent 1:0 prio 1 u32
match ip dport 443 0xffff flowid 1:10
# Direct SSH traffic (dport 22) to high-priority class 1:10
tc filter add dev ens3 protocol ip parent 1:0 prio 1 u32
match ip dport 22 0xffff flowid 1:10
# Direct traffic destined for a specific subnet to medium-priority class 1:20
# Replace 192.168.100.0/24 with your target subnet
tc filter add dev ens3 protocol ip parent 1:0 prio 2 u32
match ip dst 192.168.100.0/24 flowid 1:20
# Show all filters
tc filter show dev ens3
Step 5 — Test with iperf3
Use iperf3 to verify the bandwidth limits enforced by the class hierarchy. Run the server on the RHEL 8 host and the client on a second machine:
# On the RHEL 8 server — allow iperf3 through firewalld and start the server
firewall-cmd --temporary --add-port=5201/tcp
iperf3 -s -D
# On the client machine — run a 30-second TCP test targeting the server
iperf3 -c SERVER_IP -t 30 -P 4
# Monitor live tc statistics on the server while the test runs (separate terminal)
watch -n 1 tc -s class show dev ens3
# Check byte and packet counters per class
tc -s qdisc show dev ens3
The 1:30 default class should cap unclassified traffic near 10 Mbit/s while classified flows reach their respective limits. Observe the Sent and Drops fields in the tc -s output to confirm shaping is active.
Step 6 — Persist the Configuration
Traffic control rules are lost on reboot. Use a NetworkManager dispatcher script to reapply them automatically when the interface comes up:
cat > /etc/NetworkManager/dispatcher.d/50-tc-qos.sh </dev/null || true
tc qdisc add dev ens3 root handle 1: htb default 30
tc class add dev ens3 parent 1: classid 1:1 htb rate 100mbit
tc class add dev ens3 parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit burst 15k prio 1
tc class add dev ens3 parent 1:1 classid 1:20 htb rate 20mbit ceil 50mbit burst 15k prio 2
tc class add dev ens3 parent 1:1 classid 1:30 htb rate 10mbit ceil 100mbit burst 15k prio 3
tc qdisc add dev ens3 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev ens3 parent 1:20 handle 20: sfq perturb 10
tc qdisc add dev ens3 parent 1:30 handle 30: sfq perturb 10
tc filter add dev ens3 protocol ip parent 1:0 prio 1 u32 match ip dport 443 0xffff flowid 1:10
tc filter add dev ens3 protocol ip parent 1:0 prio 1 u32 match ip dport 22 0xffff flowid 1:10
fi
SCRIPT
chmod +x /etc/NetworkManager/dispatcher.d/50-tc-qos.sh
Conclusion
You have configured a hierarchical HTB queuing discipline on RHEL 8 that guarantees minimum bandwidth to critical classes, allows bursting to higher rates when capacity is available, and drops unclassified traffic into a low-priority default class. Filters using the u32 classifier direct SSH and HTTPS traffic to the high-priority class while bulk traffic is relegated to the default. The NetworkManager dispatcher script ensures the rules survive reboots. From here you can extend the filter set with DSCP marks, iptables mark-based classification, or more advanced qdiscs like CAKE for home or edge deployments.
Next steps: Traffic Shaping with CAKE Qdisc on RHEL 8, Using iptables MARK and tc fwmark Filters for Policy-Based QoS, and Monitoring Network Bandwidth per Application with ss and tc on RHEL 8.