Linux boxes are great at accumulating junk quietly.
Not catastrophic junk. Just enough to become annoying over time:
- stale files in
/tmp - forgotten payloads in
/var/tmp - application scratch directories that grow forever
- caches that should be disposable, but never get expired automatically
A lot of people reach for ad-hoc find ... -delete cron jobs when this happens. I think that is usually the wrong first move.
If your system already runs systemd, you probably have a better tool built in: systemd-tmpfiles.
It gives you a declarative way to say:
- create this directory if it should exist
- set the right mode and ownership
- clean old contents on a schedule
- preview what would happen before deleting anything
This guide covers the practical parts: when to use it, when not to use it, safe examples, testing, and the easy mistakes that cause surprise deletions.
What systemd-tmpfiles is actually for
systemd-tmpfiles creates, removes, and cleans files and directories based on rules from tmpfiles.d configuration.
The important pieces are:
-
tmpfiles.d(5)defines the config format -
systemd-tmpfiles(8)applies those rules -
systemd-tmpfiles-clean.timertypically runs cleanup daily -
systemd-tmpfiles-clean.servicerunssystemd-tmpfiles --clean
On this host, the shipped timer is:
[Timer]
OnBootSec=15min
OnUnitActiveSec=1d
And the service runs:
ExecStart=systemd-tmpfiles --clean
That means you often do not need to invent a custom timer just to expire old temporary files.
First, understand /tmp vs /var/tmp
This matters more than most cleanup guides admit.
The systemd project documents the intended split clearly:
-
/tmpis for smaller, temporary data and is often cleared on reboot -
/var/tmpis for temporary data that should survive reboot
The same documentation also notes that systemd-tmpfiles applies automatic aging by default, with files in /tmp typically cleaned after 10 days and files in /var/tmp after 30 days.
So if an application genuinely expects its scratch data to survive reboot, /var/tmp is the right home. If not, prefer /tmp.
That one decision alone prevents a lot of accidental foot-guns.
When to use tmpfiles.d, and when not to
Use tmpfiles.d when:
- a path should exist independent of a single service lifecycle
- you want age-based cleanup for directory contents
- you want a declarative replacement for custom cleanup scripts
- you need predictable permissions on a scratch or cache path
Do not reach for tmpfiles.d first when a service can own its own runtime/state/cache directories.
The tmpfiles.d(5) man page explicitly recommends using these service settings when they fit better:
-
RuntimeDirectory=for/run -
StateDirectory=for/var/lib -
CacheDirectory=for/var/cache -
LogsDirectory=for/var/log -
ConfigurationDirectory=for/etc
I agree with that recommendation. If the directory belongs tightly to one service, keeping that lifecycle in the unit file is usually cleaner.
Use tmpfiles.d when the lifetime is broader than one service, or the cleanup behavior needs to be more explicit.
The three line types you will use most
The full format is powerful, but most admins only need a few types.
From tmpfiles.d(5):
-
dcreates a directory, and optionally cleans its contents by age -
Dis liked, but its contents are also removed when--removeis used -
ecleans an existing directory by age without requiring tmpfiles to create it
For day-to-day cleanup policy, d and e are the stars.
Rule of thumb
- use
dwhen you want tmpfiles to create and manage the directory - use
ewhen the application creates the directory itself, but you want cleanup policy applied to its contents
A safe first example: clean an app cache after 7 days
Let us say an application writes disposable cache files to /var/cache/myapp-downloads, and you want them expired after a week.
Create /etc/tmpfiles.d/myapp-downloads.conf:
d /var/cache/myapp-downloads 0750 root root 7d
What this means:
-
dcreates the directory if missing - mode becomes
0750 - owner/group become
root:root - contents older than
7dbecome eligible during cleanup runs
Apply creation immediately:
sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp-downloads.conf
Preview cleanup behavior without deleting anything:
sudo systemd-tmpfiles --dry-run --clean /etc/tmpfiles.d/myapp-downloads.conf
Then run the cleanup for real if the preview looks correct:
sudo systemd-tmpfiles --clean /etc/tmpfiles.d/myapp-downloads.conf
Example two: clean an application-owned directory without creating it
Sometimes the app already creates the directory and you do not want tmpfiles to own that part.
In that case, use e.
e /var/lib/myapp/scratch 0750 myapp myapp 3d
This tells tmpfiles to:
- adjust mode and ownership if needed
- clean old contents in that existing directory
- leave directory creation to the application or package
This is a nice fit for scratch areas, export staging directories, or transient ingest folders.
A local demo you can test safely
If you want to see it work without touching real application data, use a disposable directory under /tmp.
TESTROOT=$(mktemp -d /tmp/tmpfiles-demo.XXXXXX)
mkdir -p "$TESTROOT/cache"
printf 'old\n' > "$TESTROOT/cache/a.bin"
printf 'new\n' > "$TESTROOT/cache/b.bin"
cat > "$TESTROOT/demo.conf" <<EOF
e $TESTROOT/cache 0755 $(id -un) $(id -gn) 0
EOF
systemd-tmpfiles --dry-run --clean "$TESTROOT/demo.conf"
systemd-tmpfiles --clean "$TESTROOT/demo.conf"
find "$TESTROOT" -maxdepth 2 -type f | sort
Why use 0 here?
Because tmpfiles.d(5) documents that for e entries, age 0 means contents are deleted unconditionally whenever systemd-tmpfiles --clean runs. That makes the demo immediate and predictable.
On my test run, the dry run reported:
Would remove "/tmp/tmpfiles-demo.../cache/a.bin"
Would remove "/tmp/tmpfiles-demo.../cache/b.bin"
That is exactly the sort of preview you want before pointing rules at real paths.
The subtle part: age is not just mtime
This is where people get surprised.
systemd-tmpfiles does not simply look at file modification time in the naive way most shell one-liners do. In debug output on this host, cleanup thresholds were evaluated using multiple timestamps.
When I tested a file whose modification time was 15 days old, tmpfiles still refused to clean it because the file's change time was new.
That matters because metadata updates can refresh eligibility in ways that are easy to miss.
So if you are testing cleanup rules, do not assume that touch -d '15 days ago' file perfectly simulates a genuinely old file for every case. Preview with --dry-run, and verify behavior against the actual directory contents you care about.
Check what your system already ships
Before writing custom rules, inspect the defaults.
Useful commands:
systemctl cat systemd-tmpfiles-clean.timer
systemctl cat systemd-tmpfiles-clean.service
systemd-tmpfiles --cat-config | less
You can also inspect vendor rules directly:
grep -R . /usr/lib/tmpfiles.d /etc/tmpfiles.d 2>/dev/null | less
This is worth doing because many packages already install sensible tmpfiles rules, and you do not want to duplicate or conflict with them.
Precedence and override behavior
tmpfiles.d(5) defines these system-level config locations:
/etc/tmpfiles.d/*.conf/run/tmpfiles.d/*.conf/usr/local/lib/tmpfiles.d/*.conf/usr/lib/tmpfiles.d/*.conf
The practical rule is simple:
- vendor packages ship rules in
/usr/lib/tmpfiles.d - local admin overrides belong in
/etc/tmpfiles.d
If you need to disable a vendor tmpfiles config entirely, the documented approach is to place a symlink to /dev/null in /etc/tmpfiles.d/ with the same filename.
A real pattern I like: expiring importer leftovers
Suppose you have a periodic import job that stages files under /var/tmp/inbox-import before moving them elsewhere.
You want:
- directory created if missing
- owned by the importer account
- stale leftovers cleaned after 2 days
Use:
d /var/tmp/inbox-import 0750 importer importer 2d
Then apply and verify:
sudo systemd-tmpfiles --create /etc/tmpfiles.d/inbox-import.conf
sudo systemd-tmpfiles --dry-run --clean /etc/tmpfiles.d/inbox-import.conf
sudo systemctl start systemd-tmpfiles-clean.service
sudo journalctl -u systemd-tmpfiles-clean.service -n 50 --no-pager
That is cleaner than a custom shell script, easier to audit, and easier to explain six months later.
What not to clean aggressively
I would be conservative around these:
- browser profiles
- databases and queues
- anything under
/var/libunless you are certain it is disposable scratch data - upload staging paths that users may still need
- application caches you have not confirmed are rebuildable and safe to lose
Also, do not treat tmpfiles.d as a magic disk-pressure tool. It is policy-based cleanup, not capacity planning.
If a path is growing because the application is misbehaving, fix the application too.
Security and correctness notes worth keeping in mind
The systemd temporary-directories guidance also warns about the shared namespace under /tmp and /var/tmp.
Two practical takeaways:
- avoid guessable file names in shared temporary directories
- prefer service isolation like
PrivateTmp=where appropriate
That is not just theoretical. Shared writable temp space is one of those places where sloppy habits become weird bugs, denial-of-service conditions, or worse.
My practical workflow
When I add a tmpfiles rule, I keep it boring:
- inspect existing rules first
- create one small
.conffile in/etc/tmpfiles.d/ - run
--createif needed - run
--dry-run --clean - test on a disposable directory before touching important paths
- check logs after the first real cleanup run
That sequence catches most mistakes before they become annoying.
Final takeaway
If you are still writing one-off cleanup scripts for every temp directory on a systemd machine, there is a good chance you are doing more work than necessary.
systemd-tmpfiles already gives you:
- declarative directory policy
- age-based cleanup
- repeatable permissions
- built-in scheduling on many distros
- a dry-run path for safer changes
That is a much nicer long-term story than a pile of fragile find commands.
Use scripts when you need custom logic. Use tmpfiles.d when what you really want is policy.
References
-
systemd-tmpfiles(8): https://man7.org/linux/man-pages/man8/systemd-tmpfiles.8.html -
tmpfiles.d(5): https://manpages.ubuntu.com/manpages/focal/man5/tmpfiles.d.5.html - systemd, "Using /tmp/ and /var/tmp/ Safely": https://systemd.io/TEMPORARY_DIRECTORIES/
- Red Hat Developer, "Managing temporary files with systemd-tmpfiles on RHEL 7": https://developers.redhat.com/blog/2016/09/20/managing-temporary-files-with-systemd-tmpfiles-on-rhel7
Top comments (0)