How to find unused images in WordPress — and delete them safely
Summary. WordPress will not tell you where an image is used. The Media Library has a column that looks like it answers that question, and it does not, which is how a lot of sites end up with holes in them. Here is where a reference to an image can actually hide, how to search each of those places for one file, and what to check before you delete anything.
Why WordPress cannot answer this for you
WordPress keeps no index of where a file is used. When you insert an image into a post, nothing writes that relationship down anywhere you can query later. The URL goes into the post content, or a block attribute, or a custom field, or a theme setting, and that is the only trace of it.
So "is this image used?" is not a lookup. It is a search across every place a theme, a plugin or a page builder might have written a URL or an attachment ID. Which is why there is no Find unused media button in the admin, and why every plugin that offers one is making an estimate, whether it says so or not.
What makes this worth doing carefully is that the two mistakes do not cost the same. Leave an unused image alone and it costs you a few kilobytes of disk. Delete one that is still referenced and you break a page. Usually a page you do not open every day, so you can be weeks away from finding out.
"Unattached" does not mean unused
The Media Library has an Unattached filter, and I think it is the most misread thing in WordPress media management.
"Attached" records the post you were editing when you uploaded the file. That is all it does. It is set once, at upload time. Nothing updates it when you use the image somewhere else, and nothing clears it when you take the image out of that post again.
So both halves of the assumption are wrong:
- Unattached images are often in use. Your site logo, a Customizer background, a header image, anything you uploaded from the Media Library screen instead of from inside a post. None of those have a parent post.
- Attached images are often unused. Upload four photos to a post, use one of them, publish. The other three stay attached to that post forever, and no page anywhere shows them.
The "Uploaded to" column is the same thing wearing a different hat. It answers a question nobody asked.
Where a reference to an image hides
Before you can search anything you need to know what you are searching. In an ordinary WordPress install, a reference lives in one of these:
- Post content. The easy one. The URL sits in
wp_posts.post_content. - Block attributes. A Gutenberg image block writes the URL into the markup and the attachment ID into the block comment, like
<!-- wp:image {"id":412} -->. Searching for the filename still finds it. A synced pattern is the awkward case, because the block lives in its own post rather than in the page you are looking at. - Post meta. This is where most of the invisible references are. An ACF image field stores the attachment ID, not a URL. Page builders serialise an entire page into a single meta value. Even a featured image is just a meta row,
_thumbnail_id. - The options table. Site logo, site icon, Customizer settings (
theme_mods_<theme>), classic widgets, and any plugin setting that lets you pick an image. - Term and user meta. Category images, author photos, anything an image-field plugin bolts onto a taxonomy or a profile.
- Theme and plugin files. A stylesheet can hardcode
url(/wp-content/uploads/2023/07/pattern.png)and nothing in the database will ever know about it. - Somewhere no scan can see. A URL assembled from parts when the page renders, or held by some third-party service. Rare. Also the reason you keep a backup instead of trusting any tool completely.
Checking one image by hand
Do this once, on a single image, even if you end up handing the job to a plugin. It shows you what a cleanup tool is actually up against.
Step 1 — get the two things you are searching for.
Open the image in the Media Library. Take the filename from the URL (old-hero-banner.jpg) and the attachment ID from the address bar (post.php?post=412). You need both, because some places store one and some store the other.
Step 2 — search the content.
In phpMyAdmin or Adminer, run a read-only query. Change wp_ if your prefix is different, and search the filename without its size suffix so the -1024x576 variants match too:
SELECT ID, post_title, post_type, post_status
FROM wp_posts
WHERE post_content LIKE '%old-hero-banner%'
AND post_status != 'trash';
Step 3 — search the meta tables, by filename and by ID.
This is the step people skip, and it is where ACF fields, featured images and page-builder layouts are hiding:
SELECT post_id, meta_key
FROM wp_postmeta
WHERE meta_value LIKE '%old-hero-banner%'
OR meta_value = '412'
OR meta_value LIKE '%"id":412%';
Step 4 — search the options table.
Logo, site icon, Customizer settings and widgets all end up here, usually buried inside serialised data:
SELECT option_name
FROM wp_options
WHERE option_value LIKE '%old-hero-banner%'
OR option_value LIKE '%"id":412%';
Step 5 — search your theme's own files.
No database query will catch a hardcoded URL in a stylesheet. From the site root:
grep -r "old-hero-banner" wp-content/themes/
If all five come back empty, the image is very probably unused. Probably, not certainly: what you have proved is that there is no evidence in the places you thought to look. That gap is the entire subject.
What to check before you delete
Five things, in this order. The first one is not optional.
- Take a backup you have actually restored before. Files and database. A backup nobody has ever restored is a wish, not a plan.
- Check drafts and scheduled posts. A post that has not gone live yet still references images, and a scan that only reads published content will call them unused. It is also why anything uploaded this morning looks unused: the post that needs it is still being written.
- Leave logos, icons and header images alone. They are usually unattached, they frequently score as unused, and they are the worst things on the site to get wrong.
- Delete in small batches. Twenty at a time, then go and load the site. If something breaks, you know which twenty did it. Delete a thousand and all you learn is that something, somewhere, is now wrong.
- Actually look at the pages afterwards. Home, one post, one landing page, and the page you open least. That last one is where the damage hides.
Make deletion a two-step operation
Here is the thing most people find out on the day they need it: WordPress does not put media in the Trash. Posts get a Trash. Media does not. "Delete permanently" means exactly that, and there is no undo behind it.
You can change that. Add one line to wp-config.php, above the line that says "That's all, stop editing":
define( 'MEDIA_TRASH', true );
The Media Library gets a Trash of its own, and deleting becomes two steps: move to Trash, check the site, empty it later. If some page turns out to need one of those files, getting it back is one click instead of a restore from backup.
Turn it on before the cleanup, not after it.
If you want a plugin to do this
Nobody is running five searches by hand across two thousand images, so at some point a tool has to do it. Judge one by these questions rather than by its screenshots:
- Does it search post meta and the options table, or only post content? Content-only scanning is where most "this plugin deleted my logo" reviews come from.
- Does it match attachment IDs as well as URLs? Custom fields and blocks store IDs, and a URL-only scan is blind to every one of them.
- Does it show you why? A verdict you cannot open up is a verdict you have no particular reason to trust.
- Does it separate "probably unused" from "safe to delete"? A stray test upload and your logo can look identical to a scanner. What tells them apart is the cost of being wrong, which is a different question from how confident the scan is.
- Does it admit when it could not finish? A scan that cannot read a page builder's format should say so and refuse, not quietly file the image under unused.
- Does it delete in one step? If it does, you have your answer.
Disclosure. I build one of these, Janitorix Media Audit. It exists because I asked those six questions about the tools I had been using and did not like my own answers. It is free and open source, and the WordPress.org review it went through is written up in what the review actually flagged. Use it or do not. The checks above work either way, and I would rather you understood them than trusted anybody's scanner, mine included.
Common mistakes
- Reading "Unattached" as "unused". The two have almost nothing to do with each other.
- Searching for the URL only. On a modern site, half the references are attachment IDs sitting in meta.
- Searching the sized filename.
hero-1024x576.jpgmisses the original and every other size. Search the stem instead. - Cleaning up a site you did not build, on the day you meet it. You do not know yet what its theme does with images. Look first.
- Doing it for speed. Nobody ever downloads an unused image, so it costs nothing in page speed. Do it for disk, for backups, for your own sanity, but do not sit there waiting for a PageSpeed number to move.
- Deleting the whole list in one go. Batches, then look at the site.
FAQ
- Does WordPress tell you where an image is used?
- No. The "Uploaded to" column records which post the file was uploaded from, nothing updates it afterwards, and there is no built-in feature anywhere in WordPress that finds unused media.
- Is an unattached image the same as an unused image?
- No. Logos, Customizer backgrounds and anything uploaded from the Media Library screen are all unattached and all in use. Treating that filter as a delete list is how sites break.
- Do unused images slow down my site?
- Not the speed a visitor sees. An image nobody links to is never downloaded. What it costs you is disk space, backup size, migration time, and a Media Library that is unpleasant to work in. Good enough reasons, but this is housekeeping, not performance work.
- Can I get a deleted image back?
- Not from WordPress. Media is deleted permanently by default. Add
define( 'MEDIA_TRASH', true );towp-config.phpand media gets a Trash, so deleting takes two steps. Without it, your only way back is a backup. - What happens to the thumbnail sizes?
- Deleting an attachment through WordPress removes the original and every registered size generated from it. Files orphaned by a theme you removed years ago are a separate problem, because WordPress has no record of them at all.
- Is there a safe way to test before deleting?
- Yes. Turn on
MEDIA_TRASH, trash a small batch, then load the site, including the pages you rarely open. Anything that broke is one click from being back.
Conclusion
A media library that has been filling up for four years is worth cleaning out. It is just worth being honest about what the job is: not a lookup, but a search across post content, post meta, options, term and user meta and the theme's own files, for a filename and an ID at the same time.
Do it with a backup you have restored before, with MEDIA_TRASH switched on, in small batches, and with the logos and icons left alone. Then the worst case is one click. Skip all that and the worst case is a broken page you find in November.