How to Configure Network QoS with tc on RHEL 7

Quality of Service (QoS) is the practice of managing network traffic to ensure that critical applications receive the bandwidth and latency guarantees they need. Linux provides a sophisticated traffic control subsystem accessible through the tc (traffic control) command, part of the iproute2 package. With tc, you can rate-limit specific hosts or ports, prioritize interactive traffic over bulk transfers, emulate unreliable network conditions for testing, and implement fair queuing across multiple flows. On RHEL 7 servers — whether acting as gateways, application hosts, or test environments — mastering tc gives you fine-grained control over exactly how every byte of network traffic is handled. This tutorial covers the core concepts and practical configurations you need to implement effective network QoS on RHEL 7.

Prerequisites

  • RHEL 7 system with root or sudo access
  • The iproute package installed (provides the tc command)
  • Basic understanding of networking concepts (IP addresses, ports, subnets)
  • A network interface to configure (examples use ens33 — substitute your own)
  • Optional: iperf3 for bandwidth testing
# Verify tc is available
tc -V
# Output: tc utility, iproute2-ss...

# Install iperf3 for testing (EPEL required)
sudo yum install -y epel-release
sudo yum install -y iperf3

# Identify your network interface
ip link show

Step 1: Understanding Traffic Control Concepts

Before configuring anything, you need to understand three key building blocks of Linux traffic control:

  • qdisc (queuing discipline): Attached to a network interface, a qdisc determines the order in which packets are dequeued and sent. Every interface has at least a root qdisc. Examples: pfifo_fast (default, simple FIFO), htb (Hierarchical Token Bucket — for rate limiting), tbf (Token Bucket Filter — simple rate limiter), fq_codel (Fair Queuing with Controlled Delay — reduces bufferbloat), netem (network emulator — for testing).
  • class: Some qdiscs (like htb) support classes, which subdivide bandwidth into hierarchical buckets. Each class can have its own rate limits and priorities.
  • filter: Rules that classify packets and assign them to a specific class. Filters can match on IP address, port, DSCP marking, or any netfilter mark.

Step 2: View and Clear Existing qdisc Configuration

# Show the current qdisc on ens33
sudo tc qdisc show dev ens33

# Show all classes (empty if using simple qdisc)
sudo tc class show dev ens33

# Show all filters
sudo tc filter show dev ens33

# Remove all tc configuration from an interface (reset to default)
sudo tc qdisc del dev ens33 root 2>/dev/null
sudo tc qdisc del dev ens33 ingress 2>/dev/null
echo "Interface reset to default pfifo_fast"

Step 3: Simple Rate Limiting with Token Bucket Filter (TBF)

The Token Bucket Filter is the simplest way to limit outbound bandwidth on an interface. It is not class-based, so it applies a single rate to all traffic on the interface.

# Limit outbound traffic on ens33 to 10 Mbps
# rate: sustained rate
# burst: maximum burst size (must be >= rate / HZ, typically >= rate/250)
# latency: maximum time a packet can wait in the queue
sudo tc qdisc add dev ens33 root tbf 
  rate 10mbit 
  burst 32kbit 
  latency 400ms

# Verify
sudo tc qdisc show dev ens33

# Test with iperf3 (run iperf3 server on another host)
iperf3 -c 192.168.1.100 -t 10

# Remove TBF when done
sudo tc qdisc del dev ens33 root

Step 4: Hierarchical Token Bucket (HTB) for Per-Class Rate Limiting

HTB is the most commonly used classful qdisc for implementing QoS policies. You define a root class with a total bandwidth ceiling, then create child classes for different traffic types, each with guaranteed and maximum rates.

# Step 4a: Add the HTB root qdisc
# default 30 means unclassified traffic goes to class 1:30
sudo tc qdisc add dev ens33 root handle 1: htb default 30

# Step 4b: Create a root class capping total at 100 Mbit
sudo tc class add dev ens33 parent 1: classid 1:1 
  htb rate 100mbit burst 15k

# Step 4c: High-priority class (e.g., VoIP/SSH): 20 Mbit guaranteed
sudo tc class add dev ens33 parent 1:1 classid 1:10 
  htb rate 20mbit ceil 100mbit burst 15k prio 1

# Step 4d: Medium-priority class (e.g., web): 50 Mbit guaranteed
sudo tc class add dev ens33 parent 1:1 classid 1:20 
  htb rate 50mbit ceil 100mbit burst 15k prio 2

# Step 4e: Default low-priority class (bulk/unknown): 10 Mbit guaranteed
sudo tc class add dev ens33 parent 1:1 classid 1:30 
  htb rate 10mbit ceil 100mbit burst 15k prio 3

# Step 4f: Add fq_codel as a leaf qdisc for each class
# This provides fair queuing and AQM within each HTB class
sudo tc qdisc add dev ens33 parent 1:10 handle 10: fq_codel
sudo tc qdisc add dev ens33 parent 1:20 handle 20: fq_codel
sudo tc qdisc add dev ens33 parent 1:30 handle 30: fq_codel

# Verify the class hierarchy
sudo tc class show dev ens33

Step 5: Adding Filters to Classify Traffic

Filters match packets and assign them to HTB classes. You can match on source/destination IP, port, protocol, or traffic control firewall marks (fwmark set via iptables).

# Classify SSH traffic (port 22) to high-priority class 1:10
sudo tc filter add dev ens33 protocol ip parent 1:0 prio 1 
  u32 match ip dport 22 0xffff flowid 1:10

# Classify SIP/VoIP (UDP port 5060) to high-priority
sudo tc filter add dev ens33 protocol ip parent 1:0 prio 1 
  u32 match ip protocol 17 0xff 
  match ip dport 5060 0xffff flowid 1:10

# Classify HTTP/HTTPS to medium-priority class 1:20
sudo tc filter add dev ens33 protocol ip parent 1:0 prio 2 
  u32 match ip dport 80 0xffff flowid 1:20

sudo tc filter add dev ens33 protocol ip parent 1:0 prio 2 
  u32 match ip dport 443 0xffff flowid 1:20

# Classify traffic from a specific IP to high-priority
sudo tc filter add dev ens33 protocol ip parent 1:0 prio 1 
  u32 match ip src 192.168.1.50/32 flowid 1:10

# View all filters
sudo tc filter show dev ens33

Step 6: Using iptables MARK with tc Filters

For complex classification rules (multi-port matching, connection tracking), use iptables to mark packets and then match on the mark in tc filters.

# Mark all bulk FTP data transfer traffic with mark 99
sudo iptables -t mangle -A OUTPUT 
  -p tcp --dport 20 -j MARK --set-mark 99

sudo iptables -t mangle -A OUTPUT 
  -p tcp --sport 20 -j MARK --set-mark 99

# In tc, use the fw (firewall mark) filter to match mark 99
# and direct it to the low-priority class
sudo tc filter add dev ens33 protocol ip parent 1:0 
  handle 99 fw flowid 1:30

# Verify the mark-based filter is active
sudo tc filter show dev ens33

Step 7: Network Emulation with netem

The netem qdisc emulates real-world imperfect network conditions, making it invaluable for testing application resilience before deploying to production.

# Add 100ms latency to all outbound traffic on ens33
sudo tc qdisc add dev ens33 root netem delay 100ms

# Add latency with ±20ms jitter (normal distribution)
sudo tc qdisc replace dev ens33 root netem 
  delay 100ms 20ms distribution normal

# Add 1% random packet loss
sudo tc qdisc replace dev ens33 root netem loss 1%

# Combine: 50ms delay with 10ms jitter and 0.5% loss
sudo tc qdisc replace dev ens33 root netem 
  delay 50ms 10ms 
  loss 0.5% 
  corrupt 0.1%

# Simulate packet reordering (5% of packets delayed by 10ms extra)
sudo tc qdisc replace dev ens33 root netem 
  delay 10ms reorder 5% 50%

# Check latency with ping during netem
ping -c 10 8.8.8.8

# Remove netem when testing is complete
sudo tc qdisc del dev ens33 root

Step 8: Making QoS Configuration Persistent

By default, tc rules are cleared when the system reboots. On RHEL 7, you can persist them using a startup script via systemd or /etc/rc.d/rc.local.

# Create a QoS setup script
sudo tee /usr/local/bin/setup-qos.sh <<'SCRIPT'
#!/bin/bash
# Network QoS setup for ens33
IFACE=ens33

# Clear existing rules
tc qdisc del dev $IFACE root 2>/dev/null

# HTB root
tc qdisc add dev $IFACE root handle 1: htb default 30
tc class add dev $IFACE parent 1: classid 1:1 htb rate 100mbit burst 15k
tc class add dev $IFACE parent 1:1 classid 1:10 htb rate 20mbit ceil 100mbit prio 1
tc class add dev $IFACE parent 1:1 classid 1:20 htb rate 50mbit ceil 100mbit prio 2
tc class add dev $IFACE parent 1:1 classid 1:30 htb rate 10mbit ceil 100mbit prio 3

# Filters
tc filter add dev $IFACE protocol ip parent 1:0 prio 1 
  u32 match ip dport 22 0xffff flowid 1:10
tc filter add dev $IFACE protocol ip parent 1:0 prio 2 
  u32 match ip dport 443 0xffff flowid 1:20

echo "QoS rules applied to $IFACE"
SCRIPT

sudo chmod +x /usr/local/bin/setup-qos.sh

# Create a systemd unit to run the script at boot
sudo tee /etc/systemd/system/network-qos.service <<'EOF'
[Unit]
Description=Network QoS Rules
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-qos.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable network-qos.service
sudo systemctl start network-qos.service

Linux traffic control via tc is one of the most powerful tools available to a RHEL 7 system administrator. With HTB classes and U32 or firewall-mark filters, you can implement sophisticated per-application or per-host bandwidth policies. The netem qdisc transforms your server into a network impairment emulator, enabling robust pre-production testing. By wrapping your rules in a systemd service, they survive reboots and remain an auditable, version-controllable part of your infrastructure configuration. As you grow more comfortable with tc, explore HFSC for latency-sensitive hierarchical scheduling and cake (Common Applications Kept Enhanced) for a modern, single-qdisc solution that handles bufferbloat, fairness, and shaping simultaneously.