TechEarl

How to Find Files Not Modified in the Last N Days (Stale File Detection)

find . -type f -mtime +30 lists every file NOT modified in the last 30 days. The +N sign means older than N days, the exact inverse of -N. The full reference for -mtime, -mmin, -newer markers, the -atime vs -mtime distinction and the noatime trap, plus the safe stale-file cleanup pattern.

Ishan KarunaratneIshan Karunaratne⏱️ 11 min readUpdated
Use find -mtime +30 to list files not modified in the last 30 days. The +N sign for older-than, -mmin for minutes, -newer markers, the -atime vs -mtime distinction, the noatime trap, and the safe stale-file cleanup pattern.

find . -type f -mtime +30 lists every file not modified in the last 30 days. The +30 reads as "modification age more than 30 days", so the result is every file older than your window. This is the expression cleanup scripts use to clear out stale logs, abandoned temp files, and old build artifacts.

The companion article covers the recent direction (-mtime -N, files modified within a window). This page is the inverse: stale-file detection with +N. The sign is the whole game. -N means recent, +N means old, and bare N means a single 24-hour slice that almost never matches what you want.

Set your values

Try it with your own values

Set your OS, search path, and the staleness threshold in days. Every find example below updates with your values.

The one-liner

bash· Linux (GNU)
find :search_path -type f -mtime +:days

That returns every regular file under your search path whose modification time is more than :days days in the past. Drop -type f if you also want to evaluate directories and symlinks.

Why +N is the inverse of -N

-mtime filters by modification age in 24-hour units, rounded toward zero. The sign in front of N picks the comparison direction:

ExpressionMeaning
-mtime -30Modified in the last 30 days (age < 30)
-mtime +30Modified more than 30 days ago (age > 30)
-mtime 30Modified between 30 and 31 days ago (age == 30 after flooring)
! -mtime -30Logical negation: NOT modified in the last 30 days

The two clean expressions, -mtime -30 and -mtime +30, are not quite a perfect partition. Files whose age floors to exactly 30 match neither, because -30 is strictly less than and +30 is strictly greater than. That 24-hour gap is the off-by-one. If you genuinely need "everything that is not recent", ! -mtime -:days is the airtight negation. It matches age >= 30 including the boundary slice, where -mtime +:days matches only age > 30.

For stale-file cleanup the gap rarely matters. A file sitting right on the 30-day line is borderline stale either way. But know the rule so a 31-day file showing up in a "31 days and older" report does not surprise you.

Minute resolution with -mmin

When 24-hour buckets are too coarse, -mmin +N is the minute-resolution variant. Same sign convention: +N for "more than N minutes ago".

bash· Linux (GNU)
find :search_path -type f -mmin +120

-mmin +120 finds files untouched for more than two hours. Useful for catching half-finished uploads or temp files that a job should have cleaned up by now.

Not modified since a specific timestamp

-newer FILE compares against the modification time of a reference file. To express "not modified since timestamp X", touch a marker file at X and negate the test with !:

bash· Linux (GNU)
touch -t 202604010000 /tmp/cutoff-mark
find :search_path -type f ! -newer /tmp/cutoff-mark

touch -t YYYYMMDDhhmm sets the marker's modification time to the cutoff. ! -newer /tmp/cutoff-mark then matches every file not newer than that marker, which is exactly "not modified since April 1". This is more precise than -mtime because it skips the day-floor rounding entirely.

-atime vs -mtime: the difference that matters

There are three timestamps on every Unix inode, and they answer different questions:

Timestampfind testUpdated when
mtime-mtimeFile contents change (a write)
atime-atimeFile contents are read (an access)
ctime-ctimeThe inode changes (chmod, chown, rename, link count)

For stale-file detection the instinct is often "find files nobody has used lately", which sounds like -atime. A file can have an old mtime but still be read every day, so -mtime alone would flag it as stale when it is actually hot.

-atime is the access-time variant:

bash· Linux (GNU)
find :search_path -type f -atime +:days

The noatime trap

Here is the catch that ruins more cleanup scripts than any other: atime is often disabled. Updating the access time on every single read is a write amplification problem, so most modern Linux distributions mount filesystems with relatime (atime updated lazily, roughly once a day) or noatime (atime never updated at all). On a noatime mount, -atime returns whatever the access time was frozen at, which usually tracks mtime and gives you a misleading answer.

Check what your mount actually does before trusting -atime:

bash· Linux (GNU)
mount | grep -E 'noatime|relatime'

If you see noatime, -atime is unreliable on that filesystem and you should fall back to -mtime, or remount with strictatime if you genuinely need access tracking. relatime is a middle ground: it updates atime only when the previous atime is older than the current mtime, so day-granularity -atime +N queries still work, but anything finer does not. Windows NTFS disables last-access updates by default for the same performance reason, which is why the PowerShell LastAccessTime filter above is just as suspect.

The safe stale-file cleanup pattern

The classic cleanup task is "delete everything older than N days in this directory". The non-negotiable habit is a dry run first. Print, eyeball, then delete.

Step one, the dry run. List exactly what would be removed:

bash· Linux (GNU)
find :search_path -type f -mtime +:days -print

Step two, once the list looks right, swap -print for -delete:

bash· Linux (GNU)
find :search_path -type f -mtime +:days -delete

find -delete has no confirmation and no undo. The find and delete files article covers the full safe-delete checklist, including the directory-deletion-order trap where -delete needs -depth to remove children before parents.

Find stale files but keep recently-accessed ones

A smarter cleanup keeps files that were read recently even if they have not been written recently. Combine an old-mtime test with a not-recently-accessed test so a stale-but-still-used file survives the sweep:

bash· Linux (GNU)
find :search_path -type f -mtime +:days -atime +7 -print

Two find tests with no operator between them mean logical AND. A file matches only if it is both older than :days by modification and untouched by reads for more than 7 days. The same noatime caveat applies: if the filesystem does not track atime, the -atime +7 clause is meaningless and you are back to a plain -mtime sweep.

macOS BSD vs GNU find

The staleness-specific differences that bite when a script moves between platforms:

BehaviorGNU findBSD find (macOS default)
-mtime +N semanticsFloors age to 24-hour unitsSame, with different fractional edge cases
-atime +NSupportedSupported
-mmin / -aminSupportedSupported
! -newer FILESupportedSupported
-newerXY (precise comparison)Supported (rare)Supported, more variants
-printf '%A@' (access epoch)SupportedNOT supported; use -exec stat -f '%a %N'
-anewer FILE (accessed since)SupportedSupported

For cleanup scripts that must run on both Linux and macOS, stay inside the portable set: -mtime, -atime, -mmin, -newer, ! -newer. Skip -printf and any GNU-only format flags.

Common mistakes

1. Confusing mtime, atime, and ctime. -mtime is content writes, -atime is content reads, -ctime is inode metadata changes. A chmod bumps ctime but not mtime. Reading a file bumps atime but not mtime. Pick the timestamp that answers your actual question, not the one that sounds closest.

2. Trusting -atime on a noatime mount. This is the single most common stale-detection bug. On noatime, access time is frozen, so -atime +30 returns files that are read constantly. Always check the mount with mount | grep atime before relying on -atime.

3. Off-by-one with +N. -mtime +30 matches age strictly greater than 30, so a file exactly 30.5 days old has age 30 and does not match. If you need the boundary included, use ! -mtime -30 instead. Same family as the bare-N trap: -mtime 30 is a single 24-hour slice, almost never what a cleanup script wants.

4. Forgetting -type f. Without it, -mtime +30 -delete evaluates directories too. A directory's mtime changes whenever a file is added or removed from it, so an old directory full of fresh files can still match -mtime +30 and trip a delete error. Always pin file-only operations with -type f.

5. Running cleanup against /. find / -mtime +30 -delete is a foot-cannon that will sweep /etc, /usr, and anything else untouched for a month. Always anchor the search at a specific directory and consider adding -xdev to stay on one filesystem.

When NOT to use this

Reach for a different approach when:

  • You need to know when a file was last used, not written, and atime is disabled. On a noatime filesystem there is no reliable last-access signal. Application-level access logs, or a fanotify/auditd watch, are the only trustworthy sources.
  • You care about content staleness, not timestamp staleness. -mtime reflects when the inode's mtime field was last bumped. A touch with no write, or an rsync that preserves timestamps, can make a file look fresh or stale independent of its contents. For real content drift, compare hashes.
  • You are detecting abandoned resources, not old files. A config file untouched for two years is not stale, it is stable. Age alone is a weak signal. Pair it with "is anything still referencing this" before you delete.
  • The filesystem updates timestamps unreliably. Some FUSE mounts, network filesystems, and container overlay layers report mtime and atime in ways that confuse find. Verify with stat before trusting a sweep.

See also

FAQ

TagsfindmtimeatimeCLILinuxmacOSBSDCleanup Scripts
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

Use find -mtime -7 to list files modified in the last 7 days. The off-by-one (-7 means under 7 days, +7 means over 7 days), -mmin for minute resolution, -newer for exact timestamps, and the BSD rounding gotcha on macOS.

How to Find Files Modified in the Last 7 Days (find -mtime)

find -mtime -7 lists every file modified in the last 7 days. The catch is the off-by-one: -7 means less than 7 days ago, +7 means more than 7 days ago, and exact-7 almost never matches what people expect. The flag reference, worked variations for hours and minutes, the BSD vs GNU rounding difference, and the safe cleanup patterns.

Use find ... -print0 | xargs -0 grep -l 'PATTERN' to find every file containing a string. When grep -r is enough, when to add find as a pre-filter for performance, multi-pattern matching with -E, and the safe NUL-delimited pipeline.

How to Find Files Containing Specific Text (find + grep)

find ... -print0 | xargs -0 grep -l 'PATTERN' finds every file containing a piece of text. The combo handles weird filenames, scales to huge trees, and replaces three other common but broken pipelines. When to use grep -r alone, when to add find as a pre-filter, and the BSD vs GNU pitfalls.

Use find -type f -name '*.txt' for one extension, or group -name tests in escaped parens joined by -o for many (.jpg, .png, .gif). Case-insensitive -iname, files with no extension, the -regex shortcut, and BSD vs GNU find differences.

How to Find Files by Extension (One or Many) with find

find . -type f -name '*.txt' lists every file with one extension. For many extensions you group -name tests with escaped parens and join them with -o. This covers the single one-liner, the multi-extension OR pattern, why the parens are mandatory, case-insensitive -iname, files with no extension at all, the -regex shortcut, and the BSD vs GNU divergence that bites on macOS.