Skip to main content

Debugging and Troubleshooting

Learning Focus

By the end of this lesson you will have a reliable, repeatable debugging workflow for any systemd.path issue — from "it never triggers" to "it loops infinitely" to "it worked yesterday but not today."

Debugging Workflow

Follow this step-by-step procedure every time a path unit misbehaves. Do not skip steps — most issues are caught in the first three.

Step 1 — Validate Unit File Syntax

step-1-verify.sh
sudo systemd-analyze verify /etc/systemd/system/myjob.path
sudo systemd-analyze verify /etc/systemd/system/myjob.service

If there are syntax errors, the output tells you the exact line and directive:

example-error.txt
/etc/systemd/system/myjob.path:5: Unknown key 'Watchh' in section 'Path'

No output means no errors.

Step 2 — Reload Unit Files

step-2-reload.sh
sudo systemctl daemon-reload
The #1 Beginner Mistake

If you edit a unit file and forget to run daemon-reload, systemd still uses the old version. This causes "I changed the config but nothing happened" confusion.

Step 3 — Check Path Unit Status

step-3-check-path.sh
systemctl status myjob.path --no-pager

What to look for:

StatusMeaningAction
active (waiting)Watch is registered, waiting for eventsGood — proceed to step 4
inactive (dead)Watch is not runningsudo systemctl start myjob.path
failedUnit failed to startCheck the error message in the status output
Not foundUnit file not foundCheck the file path and name

Step 4 — Test the Service Manually

Before trusting the path trigger, verify the service works on its own:

step-4-test-service.sh
sudo systemctl start myjob.service
journalctl -u myjob.service -n 30 --no-pager

If the service fails here, the problem is in the script, permissions, or dependencies — not the path unit. Fix the service first.

Step 5 — Trigger the Path Unit

Create the condition that should trigger the path unit:

step-5-trigger.sh
# For DirectoryNotEmpty:
touch /path/to/watched/dir/testfile.txt

# For PathExists:
touch /path/to/signal/file

# For PathChanged:
echo "test" >> /path/to/watched/file

# For PathModified:
echo "test" >> /path/to/watched/file

Step 6 — Check Both Logs

step-6-check-logs.sh
# Path unit logs (when did it trigger?)
journalctl -u myjob.path --since '1 hour ago' --no-pager

# Service logs (what happened when it ran?)
journalctl -u myjob.service --since '1 hour ago' --no-pager

# Combined view
journalctl -u myjob.path -u myjob.service --since '1 hour ago' --no-pager

Troubleshooting Matrix

Use this matrix to jump directly to the root cause:

SymptomLikely CauseFix
Path unit active (waiting) but never triggersWatched path doesn't exist and MakeDirectory= is not setAdd MakeDirectory=yes or create the directory manually
Path unit active (waiting) but pre-existing file doesn't triggerUsing PathChanged= which only fires on future changesSwitch to PathExists= for pre-existing files
Service loops infinitelyPathExists= / DirectoryNotEmpty= and script doesn't remove the triggerAdd rm or mv for the trigger file in the script
PathChanged= never firesEditor uses swap/rename (vim default)Try PathModified= or watch the directory instead of the file
Service runs but ExecStart failsWrong path, permissions, or userUse absolute paths; set User=; run chmod +x on script
systemctl status shows failedUnit file syntax errorRun systemd-analyze verify on both files
Excess triggers overwhelm the systemHot directory with rapid file creationAdd StartLimitBurst= and StartLimitIntervalSec=
Ran out of inotify watchesToo many path units or other inotify consumersIncrease fs.inotify.max_user_watches
User path unit not running after logoutLinger not enabledRun sudo loginctl enable-linger "$USER"
Service fails with "Permission denied"Running as wrong user or missing file permissionsCheck User=, Group=, and file ownership/mode
Unit loads but Triggers: shows wrong serviceUnit= directive points to non-existent serviceCheck the Unit= value or use matching base names
Path unit shows inactive (dead) after rebootWantedBy= not set or unit not enabledAdd WantedBy=paths.target and systemctl enable
"Start request repeated too quickly"Rate limit hitIncrease limits or fix the root cause of rapid triggers
Service succeeds but script output is missingWrong StandardOutput settingCheck StandardOutput= — default is journal

Common Failure Scenarios — Deep Dive

Scenario 1: "It Never Triggers"

Symptoms: Path unit is active (waiting) but dropping a file doesn't trigger the service.

Diagnostic steps:

diagnose-no-trigger.sh
# 1. Confirm the watch is active and the correct path is being watched
systemctl status myjob.path --no-pager

# 2. Confirm the watched path exists
ls -la /path/to/watched/dir/

# 3. Confirm the service name matches
systemctl show -p Triggers myjob.path

# 4. Check inotify watch count
cat /proc/sys/fs/inotify/max_user_watches
find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l

Common causes:

  1. Directory doesn't exist and MakeDirectory= is not set.
  2. Watching a symlink target instead of the symlink itself.
  3. NFS/CIFS mount — inotify doesn't work over network filesystems.
  4. inotify watches exhausted — see the inotify section below.

Scenario 2: "It Loops Infinitely"

Symptoms: The journal fills with repeated execution logs, CPU spikes, and the service never stops running.

Emergency fix:

emergency-stop.sh
# Stop the path unit immediately
sudo systemctl stop myjob.path

# Check how many times the service ran
journalctl -u myjob.service --since "10 minutes ago" | grep -c "Started"

Root cause: Your script uses PathExists= or DirectoryNotEmpty= but does NOT delete/move the trigger file.

Permanent fix: Add file cleanup to your script:

fix-loop.sh
# For PathExists:
rm /path/to/trigger/file

# For DirectoryNotEmpty:
rm -f /path/to/queue/* # or mv to done/

Scenario 3: "Permission Denied"

Symptoms: Service starts but immediately fails with exit code 126 or 13.

Diagnostic steps:

diagnose-permissions.sh
# 1. Check who the service runs as
systemctl show -p User -p Group myjob.service

# 2. Check file permissions on the script
ls -la /usr/local/bin/process.sh

# 3. Check if the script is executable
file /usr/local/bin/process.sh

# 4. Check permissions on watched directories
ls -la /var/www/drop/

# 5. Try running the script as the service user
sudo -u www-data /usr/local/bin/process.sh

Common fixes:

fix-permissions.sh
# Make the script executable
sudo chmod +x /usr/local/bin/process.sh

# Fix ownership of the watched directory
sudo chown -R www-data:www-data /var/www/drop/

# Fix the User/Group in the service file
# User=www-data
# Group=www-data

Scenario 4: "Works Manually But Not Via Path"

Symptoms: Running sudo systemctl start myjob.service works, but the path trigger doesn't activate it.

Diagnostic steps:

diagnose-manual-only.sh
# 1. Confirm the path unit is actually running
systemctl status myjob.path --no-pager

# 2. Confirm the base names match
ls /etc/systemd/system/myjob.*

# 3. Check the Triggers field
systemctl show -p Triggers myjob.path

# 4. Manually trigger and watch the journal in real time
journalctl -u myjob.path -u myjob.service -f &
touch /path/to/trigger

Common causes:

  1. Base names don't match — the .path is my-job.path but the .service is myjob.service.
  2. Unit= directive points to a non-existent or misspelled service name.
  3. Wrong directive — using PathChanged= for a file that you're creating (not modifying).

Scenario 5: "Start Request Repeated Too Quickly"

Symptoms: Journal shows start request repeated too quickly for myjob.service and the unit enters failed state.

Diagnostic steps:

diagnose-rate-limit.sh
# 1. Check the failure state
systemctl status myjob.service --no-pager

# 2. See how many times it started
journalctl -u myjob.service --since "5 minutes ago" | grep -c "Started"

# 3. Check current rate limits
systemctl show -p StartLimitBurst -p StartLimitIntervalSec myjob.service

Fixes:

fix-rate-limit.sh
# 1. Reset the failed state
sudo systemctl reset-failed myjob.service

# 2. Increase rate limits in the service file
# [Unit]
# StartLimitBurst=20
# StartLimitIntervalSec=60

# 3. Or fix the root cause — why is the directory getting so many files so fast?

inotify Watch Limits

Understanding the Limit

Each path unit uses at least one inotify watch. The system-wide default limit is usually 8192 watches. Other tools also consume watches:

  • IDE file watchers (VSCode, IntelliJ)
  • Docker
  • inotifywait scripts
  • Backup tools

Checking Current Usage

check-inotify.sh
# Current limit
cat /proc/sys/fs/inotify/max_user_watches

# Current usage (approximate)
find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l

# Which processes are using inotify
for pid in $(find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | cut -d/ -f3 | sort -u); do
echo "PID $pid: $(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ')"
done

Increasing the Limit

increase-inotify.sh
# Check current value
cat /proc/sys/fs/inotify/max_user_watches
# Default: 8192

# Increase temporarily (lost on reboot)
echo 65536 | sudo tee /proc/sys/fs/inotify/max_user_watches

# Increase permanently
echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# Verify
cat /proc/sys/fs/inotify/max_user_watches

Signs of Exhausted Watches

  • Path units show active (waiting) but never fire.
  • journalctl -u myjob.path shows inotify errors.
  • dmesg shows inotify: inotify_init: inotify instances limit reached.

Environment Differences

systemd's Environment Is NOT Your Shell

A common trap: your script works perfectly when run manually but fails inside systemd. This is because systemd services run in a minimal environment:

FeatureYour Shellsystemd Service
PATHFull path with user binsMinimal: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Home dir$HOME is setMay not be set
Working dirYour current dir/ (or WorkingDirectory=)
Shell config.bashrc loadedNot loaded
Environment varsAll your exportsOnly explicit Environment=

Fixes:

fix-environment.service
[Service]
# Set PATH explicitly
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Or load from a file
EnvironmentFile=/etc/default/myapp
# Always use absolute paths in ExecStart
ExecStart=/usr/local/bin/process.sh
# Set working directory
WorkingDirectory=/var/www/html

Network Filesystem Warning

NFS, CIFS, and Network Mounts

inotify does not work over network filesystems (NFS, CIFS/SMB, SSHFS). If your watched directory is on a network mount:

  • PathChanged= and PathModified= will never fire.
  • PathExists= and DirectoryNotEmpty= may work, but only because systemd polls them internally.

Workaround: Use a .timer unit to poll network-mounted directories instead of a .path unit:

[Timer]
OnCalendar=*:0/5 # Check every 5 minutes
Persistent=true

Diagnostic Commands Reference

PurposeCommand
Verify unit syntaxsudo systemd-analyze verify /etc/systemd/system/myjob.{path,service}
Check path statussystemctl status myjob.path --no-pager
Check service resultsystemctl status myjob.service --no-pager
View unit file contentsystemctl cat myjob.path
Show all unit propertiessystemctl show myjob.path
Show what the path triggerssystemctl show -p Triggers myjob.path
Follow live service logsjournalctl -u myjob.service -f
Errors onlyjournalctl -u myjob.service -p err --no-pager
Combined path+service logsjournalctl -u myjob.path -u myjob.service -f
Logs since specific timejournalctl -u myjob.service --since "2 hours ago"
Reset failed statesudo systemctl reset-failed myjob.service
Security auditsystemd-analyze security myjob.service
inotify watch countcat /proc/sys/fs/inotify/max_user_watches
List all path unitssystemctl list-units --type=path --all

Key Takeaways

  • Follow the 6-step debugging workflow every time — most issues are caught in steps 1–3.
  • The #1 mistake is forgetting daemon-reload after editing a unit file.
  • Infinite loops are caused by PathExists=/DirectoryNotEmpty= without file cleanup.
  • systemd's environment is minimal — always use absolute paths and explicit Environment=.
  • inotify doesn't work on network filesystems — use timers for NFS/CIFS mounts.
  • systemd-analyze verify catches syntax errors before they cause runtime failures.

What's Next

  • Study Cases — real-world scenarios where systemd.path solves complex automation problems.