systemd-tmpfiles describes filesystem objects and housekeeping rules in configuration files. A rule can create a directory with a chosen mode, while an age rule can make old entries eligible for cleanup. The command must actually run the relevant operation for either effect to occur. This is useful lifecycle policy, but it is not a continuous watcher or a hard storage quota.

When it breaks

An age is not simply “time since the file was created” or “time since its contents last changed.” The documented default considers several timestamps, including access and modification, status change for files, and creation time where available. A recently changed qualifying timestamp can preserve an entry even when its mtime looks old. Rewriting timestamps for a test can therefore change the result in an unexpected direction.

--create and --clean perform different operations. Creating the configured directory does not prove that old contents have been removed. The d rule used below keeps its directory while making eligible contents subject to age cleanup. Other types have different removal semantics; do not substitute a recursive removal rule simply because both actions sound like cleaning.

An x exclusion preserves a matching directory and its contents during age-based cleanup, whereas X excludes the directory itself without the same recursive protection. These exclusions do not override explicit r or R removal rules. Read the effective configuration and operation together before deciding which paths a maintenance run can delete.

Check it quickly

Run this Python script as an administrator in a disposable Linux environment with systemd-tmpfiles. The recorded run used Debian 12 with systemd 252.39. It passes one explicit configuration file and a temporary --root, so all created objects and deletion targets belong to the exercise. The live system’s ordinary tmpfiles configuration is not used:

from pathlib import Path
from tempfile import TemporaryDirectory
import os,stat,subprocess,time

with TemporaryDirectory(prefix='xbcatm-tmpfiles-') as directory:
    root=Path(directory);config=root/'policy.conf'
    definition='d /work 0750 - - 1s -\nx /work/keep - - - - -\n'
    config.write_text(definition)
    def apply(action):
        result=subprocess.run(['systemd-tmpfiles','--root='+str(root),action,str(config)],capture_output=True,text=True,timeout=10)
        assert result.returncode==0,result.stderr
    apply('--create');work=root/'work'
    assert stat.S_IMODE(work.stat().st_mode)==0o750
    old=work/'old';old.write_text('fixture');past=time.time()-60;os.utime(old,(past,past))
    apply('--clean');assert old.exists()
    print('old atime and mtime alone do not satisfy the default age rule')
    config.write_text(definition.replace('1s','m:1s'));apply('--clean')
    assert not old.exists()
    print('explicit mtime-only age removes the same recently created file')
    config.write_text(definition);old.write_text('aged fixture')
    keep=work/'keep';keep.mkdir();(keep/'preserved').write_text('excluded fixture')
    outside=root/'outside';outside.write_text('outside cleanup tree')
    link=work/'link';link.symlink_to(outside)
    with old.open('rb') as retained:
        time.sleep(1.2)
        fresh=work/'fresh';fresh.write_text('recent fixture')
        apply('--clean')
        assert not old.exists() and not link.is_symlink()
        assert retained.read()==b'aged fixture'
        print('unlinked file remains readable through its already open descriptor')
    assert fresh.read_text()=='recent fixture'
    assert (keep/'preserved').read_text()=='excluded fixture'
    assert outside.read_text()=='outside cleanup tree'
    assert work.is_dir()
    print('aged entries removed; recent file and recursively excluded subtree remain')
    print('symlink removed without following it; target outside the cleaned tree survives')
print('all creation and cleanup occurred in a temporary alternate root')

The create operation produces /work inside the temporary root with mode 0750. A new file then receives deliberately old access and modification timestamps. Default cleanup leaves it present because other relevant timestamps are still recent. Merely backdating mtime and atime is not enough to make this file old under the complete default rule.

Changing only the age specification to m:1s makes modification time the selected file timestamp. Cleanup now removes that same recently created file. This proves that timestamp selection changes policy, not just display formatting. The script restores the default rule afterward; mtime-only aging is an intentional alternative to assess against the application’s access pattern, not an automatic improvement.

The next phase creates an ordinary file, an excluded subtree, and a symbolic link to another harmless file outside the cleaned tree. After waiting long enough for their timestamps to age, it creates a fresh file and runs cleanup. The aged ordinary entry and symlink disappear, while the recent file and excluded subtree remain. The symlink’s target is still intact, and the configured /work directory still exists.

The ordinary file is held open across cleanup. Although its pathname is gone, its existing descriptor can still read the original bytes until it is closed. Removing a directory entry and releasing the storage backing every open reference are different events. A process holding deleted files can consequently explain why a cleanup removed names without reclaiming all the expected space.

Assertions compare actual modes, path existence, and file contents. No test treats a zero exit status alone as proof that the intended deletion happened. The entire alternate root is removed afterward, leaving the host’s existing temporary files and normal policy untouched.

Match housekeeping to the producer

Inspect the distribution’s tmpfiles setup and cleanup units to establish when these rules are invoked. Eligibility at one second does not mean deletion occurs exactly one second later; the cleanup cadence and current workload matter. An inactive timer or failed cleanup command can leave eligible files in place indefinitely.

Age policies also cannot bound how many bytes a producer creates between cleanup runs. High write volume, recent accesses, exclusions, and open descriptors all affect retained space. Use appropriate storage limits and disk monitoring when capacity must be bounded, and diagnose the producing application instead of shortening retention until useful evidence disappears.

Keep service-owned runtime, cache, and persistent state conceptually separate. Data needed after restart should not accidentally inherit a temporary-file deletion policy. When managed service directories already express the required ownership and lifetime, reuse that mechanism. Add tmpfiles rules for a concrete lifecycle requirement and verify both eligible deletion and required preservation.

Further reading

Sources