Systemd Timer with Traffic Shaping

This guide explains how to set up a scheduled task using systemd timers with automatic network bandwidth limiting. This pattern is useful for resource-intensive tasks like backups, data synchronization, or batch uploads that should run during off-peak hours without saturating network bandwidth.

Overview

The setup consists of three components:

ComponentPurpose
Service unitDefines the task to run (oneshot)
Timer unitSchedules when the service runs
Traffic control scriptManages bandwidth limiting via tc

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     triggers      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Timer     β”‚ ─────────────────▢│    Service      β”‚
β”‚  (systemd)  β”‚                   β”‚   (oneshot)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                           β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚                      β”‚                      β”‚
                    β–Ό                      β–Ό                      β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚ ExecStartPre β”‚      β”‚  ExecStart   β”‚      β”‚ ExecStopPost β”‚
            β”‚  (as root)   β”‚      β”‚  (as user)   β”‚      β”‚  (as root)   β”‚
            β”‚              β”‚      β”‚              β”‚      β”‚              β”‚
            β”‚ Enable tc    β”‚      β”‚  Run task    β”‚      β”‚ Disable tc   β”‚
            β”‚ shaping      β”‚      β”‚              β”‚      β”‚ shaping      β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. The timer triggers the service at the scheduled time
  2. ExecStartPre runs as root (+ prefix) to set up traffic shaping
  3. ExecStart runs the actual task as a non-root user
  4. ExecStopPost cleans up traffic shaping rules (runs even if the task fails)

Traffic Shaping Mechanism

The traffic control script uses Linux’s HTB (Hierarchical Token Bucket) qdisc combined with iptables cgroup matching:

  1. HTB Qdisc: Creates a traffic class hierarchy on the network interface
  2. Bandwidth Class: Adds a rate-limited class for the task’s traffic
  3. Cgroup Matching: Uses iptables to mark packets from the service’s cgroup
  4. Packet Filter: Directs marked packets to the bandwidth-limited class

This approach is superior to application-level throttling because:

  • Works with any program (no application support required)
  • Applies to all child processes automatically
  • Uses kernel-level queuing for accurate rate limiting

Service Unit Structure

[Unit]
Description=Scheduled Task with Bandwidth Limiting
After=network.target
 
[Service]
Type=oneshot
User=taskuser
Group=taskgroup
ExecStartPre=+/usr/local/bin/tc-shape.sh start eth0 50mbit
ExecStart=/path/to/your/task
ExecStopPost=+/usr/local/bin/tc-shape.sh stop eth0

Key points:

  • Type=oneshot: Service runs once and exits
  • User/Group: Run the task as a non-root user for security
  • + prefix: Run the traffic control script as root (required for tc/iptables)
  • ExecStopPost: Always runs, ensuring cleanup even on failure

Timer Unit Structure

[Unit]
Description=Daily Task Timer
 
[Timer]
OnCalendar=*-*-* 01:00:00
Persistent=true
 
[Install]
WantedBy=timers.target

Key points:

  • OnCalendar: Defines the schedule using systemd calendar syntax
  • Persistent=true: If the system was off at the scheduled time, run immediately on next boot

Common Schedule Examples

ScheduleOnCalendar Value
Daily at 01:00*-*-* 01:00:00
Every Monday at 01:00Mon *-*-* 01:00:00
Twice daily (01:00 and 13:00)*-*-* 01,13:00:00
Every 6 hours*-*-* 00/6:00:00
First day of month*-*-01 01:00:00

Traffic Control Script

The script accepts start and stop commands with optional interface and rate parameters:

#!/bin/bash
# Usage: tc-shape.sh start|stop [interface] [rate]
 
set -euo pipefail
 
ACTION="${1:-}"
IFACE="${2:-eth0}"
RATE="${3:-50mbit}"
MARK="0x12345"
CGROUP_PATH="system.slice/your-service.service"
CLASSID="1:10"
 
case "$ACTION" in
    start)
        # Set up HTB qdisc if not present
        if ! tc qdisc show dev "$IFACE" | grep -q "htb 1:"; then
            tc qdisc add dev "$IFACE" root handle 1: htb default 99
            tc class add dev "$IFACE" parent 1: classid 1:99 htb rate 10gbit
            tc qdisc add dev "$IFACE" parent 1:99 fq_codel
        fi
 
        # Add bandwidth-limited class
        if tc class add dev "$IFACE" parent 1: classid "$CLASSID" htb rate "$RATE" ceil "$RATE" 2>/dev/null; then
            tc qdisc add dev "$IFACE" parent "$CLASSID" fq_codel
        else
            tc class change dev "$IFACE" parent 1: classid "$CLASSID" htb rate "$RATE" ceil "$RATE"
        fi
 
        # Filter marked packets to limited class
        tc filter add dev "$IFACE" parent 1: protocol ip prio 1 handle "$MARK" fw flowid "$CLASSID" 2>/dev/null || true
 
        # Mark packets from service cgroup
        iptables -t mangle -C OUTPUT -m cgroup --path "$CGROUP_PATH" -j MARK --set-mark "$MARK" 2>/dev/null || \
            iptables -t mangle -A OUTPUT -m cgroup --path "$CGROUP_PATH" -j MARK --set-mark "$MARK"
 
        echo "Traffic shaping enabled: $RATE on $IFACE"
        ;;
 
    stop)
        # Remove iptables rule
        iptables -t mangle -D OUTPUT -m cgroup --path "$CGROUP_PATH" -j MARK --set-mark "$MARK" 2>/dev/null || true
 
        # Remove tc filter and class
        tc filter del dev "$IFACE" parent 1: protocol ip prio 1 handle "$MARK" fw 2>/dev/null || true
        tc class del dev "$IFACE" parent 1: classid "$CLASSID" 2>/dev/null || true
 
        echo "Traffic shaping disabled"
        ;;
 
    *)
        echo "Usage: $0 start|stop [interface] [rate]" >&2
        exit 1
        ;;
esac

Installation

# Install the traffic control script
sudo install -m 755 tc-shape.sh /usr/local/bin/
 
# Install systemd units
sudo cp your-task.service your-task.timer /etc/systemd/system/
 
# Reload systemd and enable the timer
sudo systemctl daemon-reload
sudo systemctl enable --now your-task.timer

Configuration

Network Interface

Find your interface name:

ip link show

Update the service file with the correct interface name in ExecStartPre and ExecStopPost.

Cgroup Path

The cgroup path in the traffic control script must match the service name:

system.slice/<service-name>.service

Operations

Manual Trigger

sudo systemctl start your-task.service

Check Timer Status

systemctl list-timers your-task.timer

View Logs

journalctl -u your-task.service

Monitor Traffic Shaping

# Watch bandwidth usage
watch -n1 'tc -s class show dev eth0 | grep -A4 "class htb 1:10"'
 
# View processes in cgroup
systemd-cgls /system.slice/your-task.service

Requirements

  • Linux with systemd
  • iproute2 (provides tc command)
  • iptables with cgroup match support (xt_cgroup kernel module)

Verify cgroup support:

modinfo xt_cgroup

Troubleshooting

Timer Not Firing

# Check timer status
systemctl status your-task.timer
 
# Verify timer is enabled
systemctl is-enabled your-task.timer

Traffic Shaping Not Working

# Check if qdisc is set up
tc qdisc show dev eth0
 
# Check if class exists
tc class show dev eth0
 
# Check iptables rules
iptables -t mangle -L OUTPUT -v

Permission Errors

Ensure the + prefix is used for ExecStartPre and ExecStopPost to run as root.

Security Considerations

  • Run the actual task as a non-privileged user
  • Only the traffic control setup/teardown requires root privileges
  • The cgroup isolation prevents the task from affecting other processes’ network traffic
  • Consider adding ProtectSystem=, PrivateTmp=, and other hardening options to the service unit