Skip to main content

Unit File Anatomy

Learning Focus

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:

anatomy-overview.timer
[Unit] # Metadata, dependencies, ordering
[Timer] # When to fire and scheduling behavior
[Install] # How to enable the unit on boot

Its paired .service unit has:

anatomy-overview.service
[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

DirectiveTypeDescriptionExample
OnCalendar=CalendarFire at wall-clock timeOnCalendar=02:15
OnBootSec=MonotonicFire N after bootOnBootSec=2min
OnStartupSec=MonotonicFire N after systemd startOnStartupSec=30s
OnUnitActiveSec=MonotonicFire N after last activationOnUnitActiveSec=1h
OnUnitInactiveSec=MonotonicFire N after service became inactiveOnUnitInactiveSec=20m

See OnCalendar Syntax and Monotonic Timers for deep dives.

Behavior Directives

DirectiveDescriptionDefaultExample
Persistent=Catch up missed calendar runs after downtimefalsePersistent=true
AccuracySec=Coalescing window — lower = more accurate timing1minAccuracySec=1s
RandomizedDelaySec=Add random delay up to this duration before firing0RandomizedDelaySec=5m
FixedRandomDelay=Keep the random offset stable per unit across runsfalseFixedRandomDelay=true
Unit=Explicitly name the service to activate (override default)Same base nameUnit=my-processor.service
WakeSystem=Wake system from suspend before firingfalseWakeSystem=true
RemainAfterElapse=Keep timer loaded after trigger (monotonic)trueRemainAfterElapse=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=.
persistent-example.timer
[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 Only Affects OnCalendar

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.

jitter-example.timer
[Timer]
OnCalendar=02:15
RandomizedDelaySec=5m # Add 0 to 5 minutes random delay
FixedRandomDelay=true # Same delay each run (stable per unit)
ScenarioWithout JitterWith Jitter
10 servers at 02:15All hit backup at 02:15:00Spread across 02:15:00–02:20:00
API endpoint10 simultaneous requestsRequests spread over 5 minutes
DatabaseLock contention spikeLoad 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):

ValueBehaviorUse When
AccuracySec=1min (default)May fire up to 1 minute lateMost jobs
AccuracySec=1sFires within 1 secondPrecision scheduling
AccuracySec=1usMaximum precisionBenchmarking, CI triggers

[Unit] Section

Same directives as any systemd unit:

DirectiveDescriptionExample
Description=Human-readable descriptionDescription=Run backup daily at 02:15
Documentation=Link to documentationDocumentation=man:systemd.timer(5)

[Install] Section

DirectiveDescriptionExample
WantedBy=Target to enable underWantedBy=timers.target

Complete .timer Template

complete-template.timer
[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 TypeBehaviorSuitable for timers?
Type=oneshotRuns once, exitsYes — recommended
Type=simpleStarts a long-running processRarely — only for daemons
Type=execLike simple but waits for execRarely

Why No [Install] Section

The .service should not have [Install]. It's activated by the .timer, not enabled directly at boot.

[Service] Directives Reference

DirectiveDescriptionDefaultExample
Type=Service typesimpleType=oneshot
ExecStart=Command to execute (absolute path)ExecStart=/usr/local/bin/backup.sh
ExecStartPre=Run before ExecStartExecStartPre=/usr/bin/test -d /mnt/backup
User=Run as this userrootUser=www-data
Group=Run as this grouprootGroup=www-data
WorkingDirectory=Set working directory/WorkingDirectory=/var/www/html
Environment=Set environment variablesEnvironment="PATH=/usr/local/bin:/usr/bin"
EnvironmentFile=Load env vars from fileEnvironmentFile=/etc/default/myapp
StandardOutput=Where to send stdoutjournalStandardOutput=append:/var/log/job.log
StandardError=Where to send stderrjournalStandardError=append:/var/log/job.log
RuntimeMaxSec=Kill after this durationinfinityRuntimeMaxSec=1h
Nice=Process niceness (-20 to 19)0Nice=10
IOSchedulingClass=I/O scheduler classnoneIOSchedulingClass=idle
CPUSchedulingPolicy=CPU scheduler policyotherCPUSchedulingPolicy=idle

Overlap Prevention with flock

For long-running jobs, use flock to prevent overlapping runs:

flock-example.service
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /var/lock/my-backup.lock /usr/local/bin/backup.sh
flock flagBehavior
-nNon-blocking — exit immediately if lock is held
-w 10Wait up to 10 seconds for lock
-xExclusive 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

DirectiveDescriptionExample
NoNewPrivileges=trueBlock privilege escalationAlways recommended
PrivateTmp=trueIsolated /tmpAlways recommended
ProtectSystem=strictRead-only system dirsUse with ReadWritePaths=
ProtectHome=read-onlyRead-only home dirsFor server services
ReadWritePaths=Whitelist writable pathsReadWritePaths=/var/log /mnt/backup
PrivateDevices=trueHide hardware devicesMost services
ProtectKernelTunables=trueRead-only kernel tunablesMost services

Complete .service Template

complete-template.service
[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:

create-override.sh
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

/etc/systemd/system/wp-backup.timer
[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
/etc/systemd/system/wp-backup.service
[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

  • .timer has three sections: [Unit], [Timer], [Install].
  • .service paired with a timer uses Type=oneshot and has no [Install].
  • Persistent=true catches missed calendar runs — essential for backups.
  • RandomizedDelaySec= + FixedRandomDelay=true prevent fleet load spikes.
  • Use flock -n in ExecStart= to prevent overlapping long-running jobs.
  • Security hardening should be on every production service.

What's Next