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:
| Component | Purpose |
|---|---|
| Service unit | Defines the task to run (oneshot) |
| Timer unit | Schedules when the service runs |
| Traffic control script | Manages 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 β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
- The timer triggers the service at the scheduled time
ExecStartPreruns as root (+prefix) to set up traffic shapingExecStartruns the actual task as a non-root userExecStopPostcleans 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:
- HTB Qdisc: Creates a traffic class hierarchy on the network interface
- Bandwidth Class: Adds a rate-limited class for the taskβs traffic
- Cgroup Matching: Uses iptables to mark packets from the serviceβs cgroup
- 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 eth0Key points:
Type=oneshot: Service runs once and exitsUser/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.targetKey points:
OnCalendar: Defines the schedule using systemd calendar syntaxPersistent=true: If the system was off at the scheduled time, run immediately on next boot
Common Schedule Examples
| Schedule | OnCalendar Value |
|---|---|
| Daily at 01:00 | *-*-* 01:00:00 |
| Every Monday at 01:00 | Mon *-*-* 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
;;
esacInstallation
# 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.timerConfiguration
Network Interface
Find your interface name:
ip link showUpdate 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.serviceCheck Timer Status
systemctl list-timers your-task.timerView Logs
journalctl -u your-task.serviceMonitor 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.serviceRequirements
- Linux with systemd
- iproute2 (provides
tccommand) - iptables with cgroup match support (
xt_cgroupkernel module)
Verify cgroup support:
modinfo xt_cgroupTroubleshooting
Timer Not Firing
# Check timer status
systemctl status your-task.timer
# Verify timer is enabled
systemctl is-enabled your-task.timerTraffic 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 -vPermission 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