DEV Community

Lyra
Lyra

Posted on

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

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.timer typically runs cleanup daily
  • systemd-tmpfiles-clean.service runs systemd-tmpfiles --clean

On this host, the shipped timer is:

[Timer]
OnBootSec=15min
OnUnitActiveSec=1d
Enter fullscreen mode Exit fullscreen mode

And the service runs:

ExecStart=systemd-tmpfiles --clean
Enter fullscreen mode Exit fullscreen mode

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:

  • /tmp is for smaller, temporary data and is often cleared on reboot
  • /var/tmp is 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):

  • 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

For day-to-day cleanup policy, d and e are the stars.

Rule of thumb

  • 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

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
Enter fullscreen mode Exit fullscreen mode

What this means:

  • d creates the directory if missing
  • mode becomes 0750
  • owner/group become root:root
  • contents older than 7d become eligible during cleanup runs

Apply creation immediately:

sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp-downloads.conf
Enter fullscreen mode Exit fullscreen mode

Preview cleanup behavior without deleting anything:

sudo systemd-tmpfiles --dry-run --clean /etc/tmpfiles.d/myapp-downloads.conf
Enter fullscreen mode Exit fullscreen mode

Then run the cleanup for real if the preview looks correct:

sudo systemd-tmpfiles --clean /etc/tmpfiles.d/myapp-downloads.conf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can also inspect vendor rules directly:

grep -R . /usr/lib/tmpfiles.d /etc/tmpfiles.d 2>/dev/null | less
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/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

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:

  1. inspect existing rules first
  2. create one small .conf file in /etc/tmpfiles.d/
  3. run --create if needed
  4. run --dry-run --clean
  5. test on a disposable directory before touching important paths
  6. 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

Top comments (0)