Unit File Anatomy
By the end of this lesson you will be able to write a complete .timer and .service unit pair from memory, understand every directive available in the [Timer] section, and know which service directives are critical for timer-triggered jobs.
Unit File Format
systemd unit files use the INI format. A .timer unit has up to three sections:
[Unit] # Metadata, dependencies, ordering
[Timer] # When to fire and scheduling behavior
[Install] # How to enable the unit on boot
Its paired .service unit has:
[Unit] # Metadata, dependencies, ordering
[Service] # What to run, how to run it, security
# No [Install] section — activated by the .timer unit
The .timer Unit File
[Timer] Section — Complete Reference
Scheduling Directives
| Directive | Type | Description | Example |
|---|---|---|---|
OnCalendar= | Calendar | Fire at wall-clock time | OnCalendar=02:15 |
OnBootSec= | Monotonic | Fire N after boot | OnBootSec=2min |
OnStartupSec= | Monotonic | Fire N after systemd start | OnStartupSec=30s |
OnUnitActiveSec= | Monotonic | Fire N after last activation | OnUnitActiveSec=1h |
OnUnitInactiveSec= | Monotonic | Fire N after service became inactive | OnUnitInactiveSec=20m |
See OnCalendar Syntax and Monotonic Timers for deep dives.
Behavior Directives
| Directive | Description | Default | Example |
|---|---|---|---|
Persistent= | Catch up missed calendar runs after downtime | false | Persistent=true |
AccuracySec= | Coalescing window — lower = more accurate timing | 1min | AccuracySec=1s |
RandomizedDelaySec= | Add random delay up to this duration before firing | 0 | RandomizedDelaySec=5m |
FixedRandomDelay= | Keep the random offset stable per unit across runs | false | FixedRandomDelay=true |
Unit= | Explicitly name the service to activate (override default) | Same base name | Unit=my-processor.service |
WakeSystem= | Wake system from suspend before firing | false | WakeSystem=true |
RemainAfterElapse= | Keep timer loaded after trigger (monotonic) | true | RemainAfterElapse=no |
About Persistent=true
When Persistent=true is set on a calendar timer:
- systemd stores the last trigger time on disk.
- After a reboot or downtime, systemd checks if any runs were missed.
- If runs were missed, the service fires at the next timer activation.
- The catch-up run still honors
RandomizedDelaySec=.
[Timer]
OnCalendar=02:15
Persistent=true # If server was down at 2:15 AM, run at next boot
Clearing stored state:
sudo systemctl clean --what=state wp-backup.timer
Persistent=true has no effect on monotonic timers (OnBootSec=, OnUnitActiveSec=, etc.). These timers are inherently relative and don't need catch-up logic.
About Jitter (RandomizedDelaySec)
Jitter prevents thundering herd problems when multiple servers share the same schedule.
[Timer]
OnCalendar=02:15
RandomizedDelaySec=5m # Add 0 to 5 minutes random delay
FixedRandomDelay=true # Same delay each run (stable per unit)
| Scenario | Without Jitter | With Jitter |
|---|---|---|
10 servers at 02:15 | All hit backup at 02:15:00 | Spread across 02:15:00–02:20:00 |
| API endpoint | 10 simultaneous requests | Requests spread over 5 minutes |
| Database | Lock contention spike | Load spread evenly |
About AccuracySec
AccuracySec= tells systemd how precisely to fire the timer. systemd groups timers within the same accuracy window to reduce wake-ups (power saving):
| Value | Behavior | Use When |
|---|---|---|
AccuracySec=1min (default) | May fire up to 1 minute late | Most jobs |
AccuracySec=1s | Fires within 1 second | Precision scheduling |
AccuracySec=1us | Maximum precision | Benchmarking, CI triggers |
[Unit] Section
Same directives as any systemd unit:
| Directive | Description | Example |
|---|---|---|
Description= | Human-readable description | Description=Run backup daily at 02:15 |
Documentation= | Link to documentation | Documentation=man:systemd.timer(5) |
[Install] Section
| Directive | Description | Example |
|---|---|---|
WantedBy= | Target to enable under | WantedBy=timers.target |
Complete .timer Template
[Unit]
Description=Run my job on a schedule
Documentation=man:systemd.timer(5)
[Timer]
OnCalendar=02:15
Persistent=true
RandomizedDelaySec=5m
FixedRandomDelay=true
AccuracySec=1m
# Unit=custom-service.service # Uncomment to override default name match
[Install]
WantedBy=timers.target
The Paired .service Unit File
Why Type=oneshot
Timer-triggered services run once, do their work, and exit. This maps directly to Type=oneshot:
| Service Type | Behavior | Suitable for timers? |
|---|---|---|
Type=oneshot | Runs once, exits | Yes — recommended |
Type=simple | Starts a long-running process | Rarely — only for daemons |
Type=exec | Like simple but waits for exec | Rarely |
Why No [Install] Section
The .service should not have [Install]. It's activated by the .timer, not enabled directly at boot.
[Service] Directives Reference
| Directive | Description | Default | Example |
|---|---|---|---|
Type= | Service type | simple | Type=oneshot |
ExecStart= | Command to execute (absolute path) | — | ExecStart=/usr/local/bin/backup.sh |
ExecStartPre= | Run before ExecStart | — | ExecStartPre=/usr/bin/test -d /mnt/backup |
User= | Run as this user | root | User=www-data |
Group= | Run as this group | root | Group=www-data |
WorkingDirectory= | Set working directory | / | WorkingDirectory=/var/www/html |
Environment= | Set environment variables | — | Environment="PATH=/usr/local/bin:/usr/bin" |
EnvironmentFile= | Load env vars from file | — | EnvironmentFile=/etc/default/myapp |
StandardOutput= | Where to send stdout | journal | StandardOutput=append:/var/log/job.log |
StandardError= | Where to send stderr | journal | StandardError=append:/var/log/job.log |
RuntimeMaxSec= | Kill after this duration | infinity | RuntimeMaxSec=1h |
Nice= | Process niceness (-20 to 19) | 0 | Nice=10 |
IOSchedulingClass= | I/O scheduler class | none | IOSchedulingClass=idle |
CPUSchedulingPolicy= | CPU scheduler policy | other | CPUSchedulingPolicy=idle |
Overlap Prevention with flock
For long-running jobs, use flock to prevent overlapping runs:
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /var/lock/my-backup.lock /usr/local/bin/backup.sh
| flock flag | Behavior |
|---|---|
-n | Non-blocking — exit immediately if lock is held |
-w 10 | Wait up to 10 seconds for lock |
-x | Exclusive lock (default) |
If the previous run is still holding the lock, flock -n exits with code 1 and the current run is skipped.
Security Hardening
| Directive | Description | Example |
|---|---|---|
NoNewPrivileges=true | Block privilege escalation | Always recommended |
PrivateTmp=true | Isolated /tmp | Always recommended |
ProtectSystem=strict | Read-only system dirs | Use with ReadWritePaths= |
ProtectHome=read-only | Read-only home dirs | For server services |
ReadWritePaths= | Whitelist writable paths | ReadWritePaths=/var/log /mnt/backup |
PrivateDevices=true | Hide hardware devices | Most services |
ProtectKernelTunables=true | Read-only kernel tunables | Most services |
Complete .service Template
[Unit]
Description=My scheduled job
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=www-data
Group=www-data
WorkingDirectory=/var/www/html
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ExecStart=/usr/bin/flock -n /var/lock/myjob.lock /usr/local/bin/myjob.sh
RuntimeMaxSec=1h
StandardOutput=append:/var/log/myjob.log
StandardError=append:/var/log/myjob.log
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/log /var/lock /mnt/backups
# No [Install] section — activated by the .timer unit
Drop-In Overrides
Override specific directives without editing the original file:
sudo systemctl edit wp-backup.service
# Or manually:
sudo mkdir -p /etc/systemd/system/wp-backup.service.d/
sudo tee /etc/systemd/system/wp-backup.service.d/override.conf > /dev/null <<'EOF'
[Service]
RuntimeMaxSec=2h
Nice=15
EOF
sudo systemctl daemon-reload
Full Annotated Example
[Unit]
Description=Run WordPress backup nightly at 02:15
[Timer]
# Fire at 2:15 AM every day
OnCalendar=02:15
# Catch up if the server was down at 2:15 AM
Persistent=true
# Spread load: add 0–5 minutes random delay
RandomizedDelaySec=5m
# Keep the same random offset each day
FixedRandomDelay=true
# Fire within 1 minute of the scheduled time
AccuracySec=1m
[Install]
# Start this timer when the system reaches timers.target
WantedBy=timers.target
[Unit]
Description=WordPress nightly backup
# Wait for network access
After=network-online.target
Wants=network-online.target
[Service]
# Run once and exit
Type=oneshot
# Run as www-data, not root
User=www-data
Group=www-data
# Set working directory
WorkingDirectory=/var/www/html
# Prevent overlapping runs using flock
ExecStart=/usr/bin/flock -n /var/lock/wp-backup.lock /usr/local/bin/wp-backup.sh
# Kill after 2 hours
RuntimeMaxSec=2h
# Log to file and journald
StandardOutput=append:/var/log/wp-backup.log
StandardError=append:/var/log/wp-backup.log
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/log /var/lock /mnt/backups /var/www/html
Key Takeaways
.timerhas three sections:[Unit],[Timer],[Install]..servicepaired with a timer usesType=oneshotand has no[Install].Persistent=truecatches missed calendar runs — essential for backups.RandomizedDelaySec=+FixedRandomDelay=trueprevent fleet load spikes.- Use
flock -ninExecStart=to prevent overlapping long-running jobs. - Security hardening should be on every production service.
What's Next
- Commands and Management — all the
systemctlandjournalctlcommands you need.