Tools: Essential Guide: Stop Cache Creep on Linux: Practical `systemd-tmpfiles` Cleanup Policies for `/tmp`, `/var/tmp`, and App Caches

Tools: Essential Guide: Stop Cache Creep on Linux: Practical `systemd-tmpfiles` Cleanup Policies for `/tmp`, `/var/tmp`, and App Caches

What systemd-tmpfiles is actually for

First, understand /tmp vs /var/tmp

When to use tmpfiles.d, and when not to

The three line types you will use most

Rule of thumb

A safe first example: clean an app cache after 7 days

Example two: clean an application-owned directory without creating it

A local demo you can test safely

The subtle part: age is not just mtime

Check what your system already ships

Precedence and override behavior

A real pattern I like: expiring importer leftovers

What not to clean aggressively

Security and correctness notes worth keeping in mind

My practical workflow

Final takeaway

References Linux boxes are great at accumulating junk quietly. Not catastrophic junk. Just enough to become annoying over time: 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: 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. systemd-tmpfiles creates, removes, and cleans files and directories based on rules from tmpfiles.d configuration. The important pieces are: On this host, the shipped timer is: And the service runs: That means you often do not need to invent a custom timer just to expire old temporary files. This matters more than most cleanup guides admit. The systemd project documents the intended split clearly: 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. 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: 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 full format is powerful, but most admins only need a few types. For day-to-day cleanup policy, d and e are the stars. 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: Apply creation immediately: Preview cleanup behavior without deleting anything: Then run the cleanup for real if the preview looks correct: Sometimes the app already creates the directory and you do not want tmpfiles to own that part. This tells tmpfiles to: This is a nice fit for scratch areas, export staging directories, or transient ingest folders. If you want to see it work without touching real application data, use a disposable directory under /tmp. 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: That is exactly the sort of preview you want before pointing rules at real paths. 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. Before writing custom rules, inspect the defaults. You can also inspect vendor rules directly: This is worth doing because many packages already install sensible tmpfiles rules, and you do not want to duplicate or conflict with them. tmpfiles.d(5) defines these system-level config locations: The practical rule is simple: 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. Suppose you have a periodic import job that stages files under /var/tmp/inbox-import before moving them elsewhere. Then apply and verify: That is cleaner than a custom shell script, easier to audit, and easier to explain six months later. I would be conservative around these: 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. The systemd temporary-directories guidance also warns about the shared namespace under /tmp and /var/tmp. Two practical takeaways: 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. When I add a tmpfiles rule, I keep it boring: That sequence catches most mistakes before they become annoying. 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: 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

[Timer] OnBootSec=15min OnUnitActiveSec=1d [Timer] OnBootSec=15min OnUnitActiveSec=1d [Timer] OnBootSec=15min OnUnitActiveSec=1d ExecStart=systemd-tmpfiles --clean ExecStart=systemd-tmpfiles --clean ExecStart=systemd-tmpfiles --clean d /var/cache/myapp-downloads 0750 root root 7d d /var/cache/myapp-downloads 0750 root root 7d d /var/cache/myapp-downloads 0750 root root 7d sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --dry-run --clean /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --dry-run --clean /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --dry-run --clean /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --clean /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --clean /etc/tmpfiles.d/myapp-downloads.conf sudo systemd-tmpfiles --clean /etc/tmpfiles.d/myapp-downloads.conf e /var/lib/myapp/scratch 0750 myapp myapp 3d e /var/lib/myapp/scratch 0750 myapp myapp 3d e /var/lib/myapp/scratch 0750 myapp myapp 3d 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 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 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 Would remove "/tmp/tmpfiles-demo.../cache/a.bin" Would remove "/tmp/tmpfiles-demo.../cache/b.bin" Would remove "/tmp/tmpfiles-demo.../cache/a.bin" Would remove "/tmp/tmpfiles-demo.../cache/b.bin" Would remove "/tmp/tmpfiles-demo.../cache/a.bin" Would remove "/tmp/tmpfiles-demo.../cache/b.bin" systemctl cat systemd-tmpfiles-clean.timer systemctl cat systemd-tmpfiles-clean.service systemd-tmpfiles --cat-config | less systemctl cat systemd-tmpfiles-clean.timer systemctl cat systemd-tmpfiles-clean.service systemd-tmpfiles --cat-config | less systemctl cat systemd-tmpfiles-clean.timer systemctl cat systemd-tmpfiles-clean.service systemd-tmpfiles --cat-config | less grep -R . /usr/lib/tmpfiles.d /etc/tmpfiles.d 2>/dev/null | less grep -R . /usr/lib/tmpfiles.d /etc/tmpfiles.d 2>/dev/null | less grep -R . /usr/lib/tmpfiles.d /etc/tmpfiles.d 2>/dev/null | less d /var/tmp/inbox-import 0750 importer importer 2d d /var/tmp/inbox-import 0750 importer importer 2d d /var/tmp/inbox-import 0750 importer importer 2d 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 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 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 - 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 - 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 - tmpfiles.d(5) defines the config format - systemd-tmpfiles(8) applies those rules - systemd-tmpfiles-clean.timer typically runs cleanup daily - systemd-tmpfiles-clean.service runs systemd-tmpfiles --clean - /tmp is for smaller, temporary data and is often cleared on reboot - /var/tmp is for temporary data that should survive reboot - 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 - RuntimeDirectory= for /run - StateDirectory= for /var/lib - CacheDirectory= for /var/cache - LogsDirectory= for /var/log - ConfigurationDirectory= for /etc - d creates a directory, and optionally cleans its contents by age - D is like d, but its contents are also removed when --remove is used - e cleans an existing directory by age without requiring tmpfiles to create it - use d when you want tmpfiles to create and manage the directory - use e when the application creates the directory itself, but you want cleanup policy applied to its contents - d creates the directory if missing - mode becomes 0750 - owner/group become root:root - contents older than 7d become eligible during cleanup runs - adjust mode and ownership if needed - clean old contents in that existing directory - leave directory creation to the application or package - /etc/tmpfiles.d/*.conf - /run/tmpfiles.d/*.conf - /usr/local/lib/tmpfiles.d/*.conf - /usr/lib/tmpfiles.d/*.conf - vendor packages ship rules in /usr/lib/tmpfiles.d - local admin overrides belong in /etc/tmpfiles.d - directory created if missing - owned by the importer account - stale leftovers cleaned after 2 days - browser profiles - databases and queues - anything under /var/lib unless 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 - avoid guessable file names in shared temporary directories - prefer service isolation like PrivateTmp= where appropriate - inspect existing rules first - create one small .conf file in /etc/tmpfiles.d/ - run --create if needed - run --dry-run --clean - test on a disposable directory before touching important paths - check logs after the first real cleanup run - declarative directory policy - age-based cleanup - repeatable permissions - built-in scheduling on many distros - a dry-run path for safer changes - 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