Directory / plugin

Freshet Unused Media

v1.3.0 · by Kristoff Bertram

Find unused media and unused images in the WordPress media library — checks ACF, page builders, options and raw URLs — and safely delete what isn't.

View on WordPress.org

About this plugin

WordPress’ own “Uploaded to” column only tracks where a file was first attached — it says nothing about where a file is actually used. Files referenced from ACF fields, featured images, galleries, widgets, the customizer logo, WooCommerce product galleries or plain URLs in content all look “unattached”, and genuinely unused files look no different from files your site depends on.

Freshet Unused Media scans everywhere a reference can hide and tells you, per attachment, exactly where it is used — or that it provably isn’t.

A media library cleaner that shows its evidence: find unused images, delete or remove unused media, clean up the media library and free up disk space — without deleting a file something still shows.

What it detects

  • ACF fields: image, gallery, file — including serialized values and repeater/flexible sub-fields, verified via ACF’s own field-key meta
  • ACF blocks: field values stored inside the block markup, where no URL is ever written
  • Featured images and WooCommerce product galleries
  • Block editor content: image, gallery, Cover, Media & Text and Video blocks — read from the block’s own settings as well as from the markup it renders, so an image still counts when its web address has been rewritten to a CDN
  • Classic content: [gallery] and [playlist] shortcodes, page-builder shortcodes holding an ID (image="123"), and raw file URLs, including resized variants like photo-300x200.jpg, -scaled files and percent-encoded filenames
  • Elementor page data
  • Options and theme mods: site icon, custom logo, widgets, customizer settings — serialized or JSON
  • Term meta and user meta (ACF fields on categories and profiles)
  • Term descriptions — an image or an ID in a category, tag or product-category description
  • Comments and comment meta (a file linked in a reply, review photos)
  • Excerpts, and unsaved edits held in autosaves

How it works

  • A Usage column in the Media Library (list mode) with a per-file “Check usage” action
  • A Usage meta box on the attachment screen showing the evidence: which post, which field, which option — with edit links
  • A full-library scan on the plugin’s own Unused Media screen, directly below Media in the admin menu, that batches through your library in the browser and can be stopped and picked up later
  • Safe deletion: delete selected or all unused files, each one re-checked before it goes

Why the answer can be trusted

Deleting media is destructive, so the scan is built to be wrong in one direction only — towards keeping the file.

  • Ambiguity counts as used. A bare attachment ID in a meta key the plugin has never seen keeps the file. Matches are boundary-checked, so attachment 123 is never matched by wp-image-1234.
  • Every file is re-checked in the instant before it is deleted. Anything that became used between the scan and your click is skipped rather than deleted, and the result tells you how many were skipped.
  • A reference in the trash still counts. A trashed post or comment can be restored, so the files it uses are kept.
  • Files uploaded in the last 24 hours count as in use — an editor may still be placing them — so a clean-up never removes work in progress.
  • The evidence is shown, not summarised. Each reference is listed with the object it lives in, the field or option it was found in, and a link to go and look, so you can check the reasoning before you act on it. A scan stores the first twenty per file, confirmed ones first, and the badge above the list carries the number of confirmed places found — so a heavily-referenced file says how many there are rather than letting twenty stand in for the total.
  • The reading is dated, and the screen says so. A scan reads the site’s content once and then answers file by file out of that reading, which on a large library takes hours. So the unused list describes the site as the scan read it rather than as it is this second, and both tabs print the moment it was read. A reference written while the scan was still walking the library is picked up by the next scan — and in the meantime the re-check above is what refuses to delete the file.
  • The blind spot is written down. References kept inside a plugin’s own custom database tables cannot be found by any query-based scanner, and the FAQ below names that limit rather than leaving you to discover it.

How it stays safe

The list above is what a reader can check for themselves. Underneath it are the parts that cost a file if they are wrong.

  • A database that stops answering is never read as “no references”. A failed query raises instead of returning an empty result, so 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 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 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.

The “Uploaded to” relation itself is shown as informational evidence but never counts as usage — that unreliable signal is exactly what this plugin replaces.

Thoroughness is the point rather than speed: a first full scan of a large library can take hours rather than minutes. It runs in batches that stop before the PHP time limit and resume where they left off, so a scan always makes progress and can be stopped and picked up later. Two limits on that are worth saying out loud. Every scan is a full scan — there is no incremental mode, so the second scan of a library costs what the first one did. And a scan left idle for more than a day reads the site’s content again when you resume it, rather than finishing a listing onto a reading that old; that window is configurable, below.

Extensible

Site-specific detectors can be added via the freshet_unusedmedia_detectors filter, with a complete worked example — a detector reading a plugin’s own custom table — in README.md in the plugin’s source repository; freshet_unusedmedia_is_used gets the final say on any status; freshet_unusedmedia_batch_size and freshet_unusedmedia_batch_seconds tune scan batches; freshet_unusedmedia_upload_grace sets the recent-upload window in seconds (0 disables it); freshet_unusedmedia_index_max_age sets how long a scan’s reading of the site may sit idle before resuming it reads the site again instead (default 24 hours, 0 disables the check).

Part of the Freshet plugin suite. Full documentation: freshet.studio/docs. Questions and problems: freshet.studio/support.

Plugin tags