A client asked me the ordinary version of this question: can we clear out the media library, it has gotten out of hand. The site is large, multilingual, and has been updated continuously since 2018 by more people than anyone can now list. Nobody could tell me which files were still in use. Not the editors, not the previous agency, certainly not WordPress.
That's the gripe, and why I ended up writing Freshet Unused Media. WordPress does not know whether a file is used. It has never known. Everything below follows from that.
What was in the library
Measured on an offline copy, before anything was touched:
- 38,797 attachment rows, none of them in the trash.
- 3,656 actual files on disk. The site is multilingual, so a single upload carries roughly ten attachment rows — one per language. The media library screen counts the rows; the disk holds the files. Those are two very different numbers and only one of them is the work.
- Ten gigabytes of uploads, generated thumbnail sizes included.
- Eight years of it, 2018 to 2026, with uploads in every single year — no quiet period, no natural cut-off to delete behind.
- By type: about 27,000 image rows, 6,209 PDFs, some 2,500 presentation decks, a couple of hundred videos.
And the number the whole job turns on: 23,659 of those rows — three in five — have no parent post. In wp-admin they appear under Media → Unattached. If you have ever opened that filter on a big site and felt a flush of opportunity, this is that feeling at scale: three fifths of a ten-gigabyte library, apparently free for the taking.
It is not, and the reason is worth being precise about.
“Unattached” does not mean unused in WordPress
WordPress records exactly one relationship for a file: the post you happened to have open when you uploaded it. That is the Uploaded to column, and it is the entire extent of core's knowledge about where a file lives. It is an upload habit, not a usage record.
Which means every one of these is used and reads as Unattached:
- an image set in a custom field, a repeater row or a gallery field;
- a file referenced inside block or page-builder markup that stores only the attachment ID and never writes a URL;
- a PDF linked as a plain URL somewhere in post content;
- a logo set in a theme setting or the customizer;
- an image in a widget, a term description, a category header, a user profile.
And the inverse holds too, which people forget: a file can be neatly attached to a post that was deleted, redirected or emptied years ago, and be referenced by nothing at all. Attachment status and usage are unrelated facts. Sorting by one to reason about the other is the single most common way a media cleanup takes a site down.
So the Unattached filter is not a cleanup tool. It is a filter on a column that answers a question nobody is asking.
The PDFs stranded on retired product pages
Here is the first shape the problem actually took. This estate documents products, and products get retired. The page comes down, or gets unpublished, or is replaced by the next model's page. What does not come down is the paperwork: the spec sheet, the installation manual, the brochure — each in several languages, each uploaded separately, each now pointing at nothing.
A few thousand PDFs in a library that size, and a real proportion of them are exactly that: documentation for something that is no longer sold. Genuinely dead weight. The kind of thing this exercise exists to remove.
Except that sitting immediately beside them, indistinguishable in every column wp-admin offers, is the other kind: the retired product whose manual is still linked from a downloads listing, because people who bought the thing in 2019 still own it and still need the manual. Same age, same folder, same Unattached, same greyed-out Uploaded to. One is safe to delete. The other is a live download a customer will hit next week, and deleting it produces a 404 that nobody notices for six months.
You cannot tell them apart by looking at the media library. There is no view, no column, no sort order that separates them. The only thing that separates them is a search of the site's own content — and that is a query problem, not a housekeeping problem.
The old logos, and finding where an image is used
The second shape was smaller and much more annoying. There had been a brand refresh. Somewhere in the library were at least three generations of the same logo, and nobody could say which files were current.
They were called what these files are always called —
logo.png, logo-2.png, logo-new.png,
logo-final.png, plus the copies re-uploaded by four different people over
four years, each landing as logo-1.png through logo-8.png.
Several were within a few pixels of each other. Thumbnails at 60 px told me
nothing.
Note what the actual question is, because it is not the one a cleanup plugin normally answers. It was never “is this file safe to delete”. It was the step before, and it is harder:
Which of these is the old logo — and where is it still being shown?
That is a reverse lookup: given a file, list every place on the site that references it. WordPress has no such feature. Not in the media modal, not in the attachment edit screen, not anywhere. The information exists — it is scattered across post content, postmeta, options, term meta and widget records — but nothing surfaces it.
Once you have that list, the problem stops being scary and becomes a morning's work: twelve places, go and change them, then the file is genuinely unreferenced and deleting it is a fact rather than a gamble. Without it, replacing a logo across a site that size is guesswork with a comms deadline attached. That reverse view is the one thing I built specifically for this job, and it is why the plugin's Used view exists at all: it lists every reference with the object it lives in, the field it was found in, and a link to go and look at it.
The downloads nobody links to any more
The third shape is the one with no story behind it, which is exactly why it accumulates. A campaign ran in 2020 and had a whitepaper. A page was rebuilt in 2022 and the new version dropped two of the five attachments the old one carried. A landing page was replaced. Somebody uploaded a file, then uploaded it again a minute later because the first attempt seemed not to have worked.
None of that is a decision anyone made. It is orphaning by neglect, and over eight years it is most of what a large media library actually is. It is also the only category here that is unambiguously safe to remove — once, and only once, you can demonstrate that nothing points at it.
What a scan has to look at to answer the question
All three shapes reduce to one query: does anything, anywhere in this database, still refer to this file? Answering it honestly means looking in every place a reference can hide, and there are more of them than people expect:
- Post content — raw URLs, including every generated size and the
-scaledvariant, plus thewp-image-123class WordPress writes into editor markup. - Custom fields — checked against the field keys themselves, not by hoping an ID appears in a value somewhere.
- Block and page-builder markup, where frequently no URL is ever stored — only an attachment ID inside a JSON payload under an arbitrary key.
- Options and theme mods — where site logos, favicons and header images live, all of which read as unattached forever.
- Widgets, term meta, term descriptions, user meta, comments — the long tail that no filename search covers.
- Autosaves and revisions, so restoring a draft does not resurrect a reference to a file you deleted last week.
And it has to be wrong in one direction only. Ambiguity counts as used.
A reference from a trashed post still counts. Files uploaded in the last day are held
back, because a file that is mid-workflow looks exactly like a file nobody wanted.
Attachment 123 is never matched by wp-image-1234. If the plugin is going
to be wrong, it must be wrong by keeping a file you did not need, never by deleting one
you did. The
detection rules are written out in the docs.
There is one thing this approach genuinely cannot do, and it should be said plainly
rather than discovered: a plugin that stores its references in its own custom
database table is invisible to any query-based scanner, mine included. If a
slider or a form builder keeps attachment IDs in a private table, no amount of
searching wp_posts and wp_postmeta will find them. That limit
is documented, and there is a filter to register a detector of your own that queries
your table and reports back like a built-in one. It is a blind spot with a door in it,
but it is still a blind spot.
How long a scan of a large media library takes
Honestly: a long time, and I would rather say so here than have you find out at 2AM.
On this library, a laptop, and a random draw of files taken with all their language siblings, the measured rate was about 14 seconds per file. For 3,656 files that is roughly fourteen hours for the first full pass.
The cost is structural, not a defect: every file group is checked against whole-table
searches across a wp_postmeta of several hundred thousand rows and a
wp_posts of nearly a hundred megabytes, several times over, for each of
the reference shapes above. Raising the database buffer pool from 128 MB to
3 GB moved it from 4.5 to 3.2 seconds per row and then stopped mattering, because
the work is CPU-bound pattern matching rather than disk.
Thoroughness is the product; the hours are what it costs. A media scanner that finishes a library like this in ninety seconds has not looked in the places where the references actually were. The first scan is a job you start and walk away from. Everything after that is cheap.
What the scan actually found, and it is not what I expected
Here is where a marketing page would like me to say and it freed nine gigabytes. It did not, because nothing was deleted — this run was scan-only by design, the deletions are the client's to make deliberately — and because the first results pointed somewhere more interesting.
Of a random sample of 450 files — about one in eight of the library, drawn with all their language siblings — 438 were used and 12 were unused. Looked at through the Unattached filter, it is starker: 98% of the unattached rows in that sample sat on a file that something on the site still referenced. It is a sample, not the full pass, and I am not going to dress it up as more than that — but the direction is clear and it is worth sitting with:
A library where three files in five are marked Unattached turned out to be almost entirely referenced.
Look at what those references were. The genuine ones were a file in page content, an image in a custom block field, a featured image, a masthead image in a theme field — every one of them invisible to Uploaded to. And a large share of the rest were other plugins' bookkeeping: an offload plugin's cache row, a translation plugin's media record, an image-optimisation plugin's index. Those are ambiguous evidence, and ambiguous evidence counts as used, on purpose. Nineteen in twenty of the used verdicts carried at least one real reference; the rest rested on bookkeeping rows alone.
So the outcome of this exercise was not a large number of deletions. It was that a question nobody could answer became answerable: for any file in that library, here is the list of places it is referenced, with a link to each one, and here is the much shorter list of files where that list is empty. The retired-product PDFs can now be separated from the still-linked ones by reading, not by guessing. The old logos can be found and replaced before anyone argues about deleting them.
That is what the plugin solves. Not to delete a lot but to stop guessing.
How to clean up a WordPress media library without breaking the site
Whatever tool you use, mine included, the order of operations is the same:
- Do not trust the Unattached filter. It answers a different question. Treat it as trivia, not as a work queue.
- Back up the database and the uploads directory first. Deleting an
attachment in WordPress is permanent — there is no trash for media unless you have
turned on
MEDIA_TRASH, and most sites have not. - Scan, then read the evidence. A count of unused files is not a result. The list of where each reference was found is the result, and it is the thing you check before you act on it.
- Start with the categories you can reason about — the documentation for products that no longer exist, the superseded logo, the campaign that ended. Those you can verify by eye in minutes.
- Delete in batches, and re-check at the moment of deletion, so anything that became used between the scan and the click is skipped rather than removed.
- Expect to keep most of it. If a scan tells you that ninety percent of your library is deletable, the scan is not looking hard enough.
Common questions
Does “unattached” mean unused in the WordPress media library?
No. Unattached only means the file was not uploaded from inside a post editor. Files used in custom fields, page builders, widgets and theme settings are shown as unattached while being displayed on the site every day.
Is it safe to bulk delete unattached media in WordPress?
No. On the library described here that would have meant deleting 23,659 rows, of which the sample says 98% sat on a file that was still referenced somewhere. Deletion is permanent by default.
How do I find out where an image is used in WordPress?
Core offers no reverse lookup at all. Finding it means searching post content, custom fields and their field keys, block markup that stores only an attachment ID, options and theme mods, widgets, term and user meta, comments and revisions — including every resized variant of the filename. That is what Unused Media's Used view lists for a given file.
Why does deleting unused images not free as much space as expected?
Because most of the size is in the generated thumbnail sizes rather than the originals, and because — as above — far less of the library is genuinely unused than the Unattached count implies. On this estate the originals accounted for roughly a third of the ten gigabytes.
How long does a first scan take on a large library?
Hours. Around fourteen for 3,656 files on my laptop here. Later scans are fast; the first one is thorough by necessity and there is no honest way around it.
Freshet Unused Media scans, detects and deletes for free — nothing in the download is locked or time-barred. The Used view, WP-CLI, the evidence export and the space totals are the paid additions, and none of them delete anything. See what it does, or read how detection works before you trust it with anything.
The library in this piece belongs to a client and is not named. Every figure was measured on an offline copy of it; nothing was deleted during the scan, and the effectiveness figures come from a sample of 450 files rather than a completed pass.