Deleting safely
Deletion happens in one place — Unused Media → Unused — and only when you ask for it. Nothing is deleted on a schedule, nothing is deleted by a scan, and there is no delete verb on the command line. This page is the one to read before you press anything.
The two actions
Delete selected — tick the rows you want and press it. The button sits above and below the table, so it is in reach from either end of a long list.
Delete all unused — the button on the heading row, deliberately styled as a destructive link rather than as the screen's primary action. The number in the label is the set it will act on.
Both re-check first (below), both report what happened, and both obey the media trash.
Filters, and why the button label changes
Above the list there is a filter bar: uploaded from / to, filename contains, min size, max size. It is a plain GET form, so the URL is the filter state — it survives pagination and can be bookmarked.
When a filter is active, three things change together:
- the heading counts both numbers — Unused files (37 of 400) — so a subset can never be misread as the whole library;
- the delete-all button becomes Delete all matching (37);
- its confirmation spells the filter out in words: Delete all 37 unused attachments matching the current filter (uploaded on or after 2024-01-01, at least 2 MB)?
A control that still said all over a screen showing a subset is exactly how the wrong files get deleted, so it doesn't.
One caveat the screen states for itself: while a size filter is applied, files whose size cannot be read are left out of the list rather than being counted as zero.
Every file is re-checked in the instant before it goes
The list you are looking at reflects the last scan. Between that scan and your click, someone may have placed one of those files in a post.
So the delete pass does not trust the list. Each ID is scanned again, right there, and anything that has become used is skipped rather than deleted. The result says so:
Deleted 12, skipped 1 (found in use on re-check), failed 0.
Every confirmation dialog states the promise before you accept it:
Each one is re-checked first; anything still in use is skipped.
One honest limit: the re-check is not atomic. A reference written in the fraction of a second between the re-check and the deletion is not seen. The upload grace covers the realistic version of that race — a file placed in the editor before its post is saved — and MEDIA_TRASH covers the rest.
Trash, or permanence
The plugin has no trash of its own and does not want one. It respects WordPress'.
With MEDIA_TRASH enabled, deleted files go to the media trash and every confirmation ends:
Deleted files go to the media trash and can be restored from there.
Without it, deletion is permanent, and the confirmation says exactly that:
Deletion is permanent and cannot be undone.
The Unused tab also carries a standing notice above the list when there is no safety net: MEDIA_TRASH is not enabled — deletions are permanent. Add define( 'MEDIA_TRASH', true ); to wp-config.php to get a trash safety net.
That is the whole safety story, and it is deliberately short: respecting the setting WordPress already has, and saying which side of it you are on at the moment you press the button. If you are about to clean a library you cannot re-upload, enable it first.
What deletion does and does not touch
- It deletes attachments through WordPress' own deletion, so files, generated sizes and metadata go with the post — trashed or erased, according to
MEDIA_TRASH. - It never deletes a file it has not just re-verified as unused.
- It records the measured size of what it removed, so the space totals [Pro feature] can report it. With
MEDIA_TRASHon, files that went to trash are not counted as reclaimed — nothing is freed until the trash is emptied. The size is credited once WordPress reports every library entry for the file removed; a file WordPress could not remove from disk is credited nothing and named as “left on disk” on the delete notice. - It changes nothing else on the site: no posts are edited, no options rewritten.
How it stays safe
The things above are what you can check for yourself. These are the ones that cost a file if they are wrong, and they are worth knowing about before a clean-up rather than after.
- A database that stops answering is never read as no references. A failed query raises instead of returning an empty result: the file keeps whatever status it had and the screen says the check could not be made. The quiet short answer is the one that deletes a file somebody needs.
- The unit of work is the file on disk, not the library entry. Several entries can point at one file — a duplicate import, a translation copy — and the file goes only once every entry standing on it is unused.
- Files that collide by name are held apart. An upload whose own name is already size-shaped, or a scaled upload whose original is some other entry's own file: both are cases WordPress unlinks by name without checking, including names that differ only in case. Those relations are read before anything is removed.
- The disk is read, not only the database. A generated size that has dropped out of an attachment's stored metadata — after a theme change, an import, a thumbnail rebuild — is still one of that file's names, and a page can still be showing it. Backup sizes, the sources behind WebP and AVIF copies, and percent-encoded filenames are read the same way.
- Size files whose original is gone are listed and never deleted. There is no library entry to corroborate them against, so they are reported for a person to judge rather than swept.
- The plugin's own bookkeeping is outside what it reads. Its cursor, tallies and notes live in the same tables the scan searches, and a file was once reported used on the strength of the scan's own numbers.
- A delete run is bounded by the clock, and the re-check is not what gives way. A deletion is one request with no second half, so it stops and hands the rest on while there is still time to tell you what it did, rather than being killed part-way having reported nothing. Nothing about the re-verification is skipped to fit a budget.
- A persistent object cache is never emptied. A long run releases its own memory without throwing away the cache every visitor to the site is being served from.
Before a big clean-up
- Enable
MEDIA_TRASH, or take a backup you have actually restored from before. - Run a full scan and let it finish — the Not scanned count on the Scan tab should be zero.
- If a plugin on your site keeps media references in its own tables, check what it holds (the blind spot).
- Spot-check a handful of files on the Unused list: open one, read the Usage box, agree with it.
- Then delete — filtered first, if you want to start small.
Next: What a license adds.