Switch a theme and the advice arrives within a minute: regenerate your thumbnails. Nobody says what that actually does, how long it takes on a real library, or which files it leaves behind.
A regeneration is not a repair. WordPress re-reads the original file for every attachment you point it at, then writes a fresh set of derivative files at whatever sizes are registered right now. Nothing is inspected, nothing is diagnosed, nothing is optimised.
That is genuinely useful about five times in the life of a site, and a wasted afternoon the rest of the time. It also has one behaviour that decides whether your uploads folder shrinks or grows during the job, and it is hidden inside the flag most tutorials tell you to add.
What you are regenerating
A thumbnail in WordPress is not a cached rendering or a database blob. It is a separate JPEG, PNG, WebP or AVIF file sitting in the same uploads folder as the file you uploaded, with the dimensions baked into its filename: holiday.jpg, holiday-150x150.jpg, holiday-300x200.jpg, and so on. Which ones exist is recorded in the attachment’s _wp_attachment_metadata row, in a sizes array keyed by size name, each entry holding the file name, width, height and MIME type.
Those two things, the files and the metadata, are what a regeneration rebuilds. If you want the full picture of how one upload becomes eight files in the first place, that is a topic of its own, covered in how WordPress image sizes actually work. The short version is that get_intermediate_image_sizes() in wp-includes/media.php starts with four names, thumbnail, medium, medium_large and large, then merges in everything registered with add_image_size() by your theme and plugins. Core’s defaults are 150 by 150 hard cropped, 300 by 300, 768 wide and 1024 by 1024. Only three of those four have a control on Settings, Media. There is no field for medium_large anywhere in wp-admin.
When the rebuild runs, WordPress asks wp_get_registered_image_subsizes() what should exist today, opens the source with an image editor, and calls make_subsize() once per size. It writes the metadata back after every single sub-size rather than at the end, which is why an interrupted run leaves a partial sizes array instead of nothing at all. One filter is worth knowing in that path, intermediate_image_sizes_advanced, which lets you drop a size from a generation run without unregistering it.
One behaviour surprises people every time: core skips any registered size that is larger than the source. Each candidate goes through image_resize_dimensions(), and the ones that would mean scaling up are dropped, under a comment in wp-admin/includes/image.php that reads “Skip registered sizes that are too large for the uploaded image”. Upload a 900 pixel wide photo and you will never get a large file for it, however many times you regenerate. That is not a failure, it is core declining to invent detail that is not there.
The five situations that justify it
A new size has been registered. add_image_size() only affects uploads that happen after it runs. Every image already in the library has no file for the new size, so the theme falls back to something else, usually the full size, and your grid loads 2560 pixel images into 400 pixel boxes.
The dimensions or the crop of an existing size changed. Either through Settings, Media, or through the third argument of add_image_size() flipping from false to true, or to a position array like array( 'center', 'top' ). The old files still exist at the old shape and core will happily keep serving them.
You switched theme and the new one registers a different set. This is the classic trigger, and it is also the one most often invoked when nothing needs doing at all. More on that below.
Sub-sizes failed to generate at upload time. Common on constrained hosting: a large upload hits the PHP memory ceiling or the request timeout partway through, and because metadata is saved after each sub-size, the sizes array ends up short rather than absent. Regeneration is the correct fix here, and it is the one case where it reliably repairs something.
The output format changed. If you have filtered image_editor_output_format so that JPEG uploads produce WebP sub-sizes, existing attachments keep their old JPEG derivatives until you regenerate. Read the format comparison before you commit to that filter, because it changes the URL of every sub-size on the site.
When it changes nothing
Most theme switches do not need a regeneration. A large share of themes use core’s four sizes and control appearance with CSS container widths and object-fit, which are not files and cannot be regenerated. If the new theme registers no sizes of its own, you can run the command for six hours and get a byte-identical result.
Regeneration also does nothing for a wrong sizes attribute, which is the usual reason a browser downloads a file that is far too big for the slot it lands in. That is a markup calculation, not a file on disk. It does nothing for images that look soft because they were exported soft. And it does nothing for layout: if the image is squashed, the problem is CSS, not pixels.
The honest test is to look at what is registered before you commit to anything. Two read-only commands tell you which sizes will be written, with their current dimensions and crop settings, and how many attachments are in scope.
# Print every registered image size with its current settings.
# Read only, changes nothing.
wp eval 'print_r( wp_get_registered_image_subsizes() );'
# How many image attachments are we talking about?
wp post list --post_type=attachment --post_mime_type=image --format=count
Before you change any dimensions, it is worth knowing how many image URLs are already written into your content by hand, because those are exactly the ones regeneration will not help.
Paste a post below, or the rendered HTML of a page. The scan finds every reference with a size suffix in it, in src, srcset, background images and block comments, tells you which of them match a size you still have registered, and lists the registered sizes that appear nowhere at all.
Regenerate impact scan
Regenerating thumbnails rebuilds the files on disk, it never rewrites a URL that sits hard coded in a post. This scan reads post content, rendered HTML or an export file and lists every image reference that would break if the registered sizes changed. Everything is read in this browser tab: no file is uploaded, and nothing is fetched from anywhere.
or press Enter to pick one. An .html, .xml or .txt file is read here in the tab and never sent anywhere.
No file read yet.
6 sizes read.
A height of 0 means the height is not capped. The command wp media image-size lists the sizes your site really registers.
| Where | File and what happens | Size | Registered | Risk |
|---|
The three chips above filter the table only. The five counters always cover the whole scan.
A size missing from this text is not proof that it is unused: archives, widgets, feeds and themes ask for sizes too. Treat it as a shortlist to check before you switch one off and win back disk space.
The command, flag by flag
WP-CLI ships wp media regenerate in core, no plugin needed. Run it with no arguments and it targets every image attachment on the site, after a confirmation prompt. The examples below are from WP-CLI 2.12, and the flag behaviour described here was checked against that version’s source.
# Everything, with a confirmation prompt.
wp media regenerate
# Specific attachments only. Safest way to see the result first.
wp media regenerate 1204 1205 1206
# Skip the prompt. Required for anything unattended.
wp media regenerate --yes
# Only images whose sizes are new, changed or missing from disk.
wp media regenerate --only-missing --yes
# One size only. Cuts the work by the number of sizes you skip.
wp media regenerate --image_size=large --yes
# Keep the old sub-size files instead of deleting them first.
wp media regenerate --skip-delete --yes
# Delete sub-sizes whose name is no longer registered. Regenerates nothing.
wp media regenerate --delete-unknown --yes
The default run is destructive in a specific, useful way. Before it writes anything, the command walks the attachment’s existing sizes array and unlinks each file it names, then hands the source to wp_generate_attachment_metadata(), which builds a fresh sizes array from an empty starting point. Old files out, new files in, metadata replaced wholesale. There is no undo, so take a backup of the uploads directory and the database before you start, and run it against a single attachment ID first.
--yes answers the confirmation. The prompt only appears when you pass no attachment IDs, which is a small mercy, because that is exactly the run you do not want to start by accident.
--image_size=large restricts the work to one registered size, and it changes the source file too: for a single size the command deliberately uses the file WordPress normally serves rather than the pre-scaled original, so the run cannot re-create the scaled copy or rotate anything behind your back. If you changed one number on Settings, Media, this turns a seven size job into a one size job.
--skip-delete leaves the previous sub-size files in place instead of unlinking them. Reach for it when files are hotlinked from somewhere you do not control, or when old post HTML has hardcoded sub-size URLs. The cost is duplication on disk, so it is a deliberate trade rather than a safety default.
What “missing” actually means
This is where the advice on the internet splits, because two different pieces of code answer the question and they answer it differently.
Core’s own answer is name-only. wp_get_missing_image_subsizes() in wp-admin/includes/image.php works out what to build with array_diff_key() against the existing sizes array, under a comment that spells out the limitation: it only checks for matching size names, the dimensions for a size name may have changed, and core keeps the old sub-sizes anyway because the image may have been used in an older post. _wp_make_subsizes() repeats the same rule further down the file. So anything built on that path, including the repair that core itself offers for a failed upload, will not notice that medium went from 300 to 400.
WP-CLI does not rely on that. Its --only-missing runs its own comparison first: it resolves the registered sizes for that specific attachment, then checks for size names the metadata does not have, for recorded widths and heights that differ from the registered ones, and finally for recorded files that are no longer on disk. Any of the three counts as work to do. Change medium from 300 to 400, run wp media regenerate --only-missing, and it will correctly pick the change up.
The catch is elsewhere, and it is not in the help text. Passing --only-missing silently switches --skip-delete on. The two flags are not independent: asking for the cheap run also asks for the run that never deletes anything. That is defensible on its own, since a partial repair should not destroy files it is not replacing, but it means the flag most often recommended as the safe default is the one that guarantees leftovers.
The files nobody deletes
Here is the part that turns a tidy-up into a disk problem. Every deletion WordPress and WP-CLI perform is driven by the attachment metadata. The command deletes what the sizes array names. It does not scan the folder, it does not match filename patterns, and it has no other way of knowing that a file belongs to an attachment at all.
That works fine on a plain run. The old theme registered six sizes, those six are still listed in the metadata, the command unlinks all of them and writes the new set. It falls apart the moment the metadata is rewritten while the files stay put, which is exactly what --only-missing and --skip-delete arrange. The regeneration replaces the sizes array with one built from today’s registered sizes, the entries for the old theme’s sizes vanish from the record, and the files they pointed at stay in the uploads folder referenced by nothing.
Put numbers on it. Old theme registers six custom sizes on top of core’s four, library of 3,000 images, so roughly 30,000 derivative files. New theme registers three. You run --only-missing because a tutorial said it was faster, and afterwards there are new files for the three sizes, no metadata pointing at the old six, and 18,000 files still on disk that nothing on the site will ever reference again. Your uploads folder grew during what you thought was a cleanup, and so does every backup from now on.
WP-CLI does have --delete-unknown for this, and two things about it matter. It regenerates nothing at all: when you pass it, the command deletes the unregistered sub-sizes for each attachment, logs what it removed, and moves to the next ID. And it reads the same metadata everyone else reads, so it can only remove files that are still listed in the sizes array. Run it before the regeneration and it works. Run it after a regeneration that already rewrote the metadata and it finds nothing, because from WordPress’s point of view those files stopped existing when their entries did.
So the order is the whole trick: back up, delete the unknown sizes first, then regenerate. Anything that slipped through earlier, from a migration, from a previous run with --skip-delete, or from a plugin that wrote its own derivatives, is now invisible to every flag in the command and can only be found by comparing the uploads directory against the database. That is its own discipline, and the order that avoids breaking live pages is set out in the guide to cleaning up a WordPress media library. Do not skip the deliberate part: a URL like photo-720x480.jpg can be hardcoded in a five year old post, buried in page builder JSON, stored in a custom field, or referenced from an email template, and deleting it breaks all of those silently.
If you are dropping a size for good, unregister it properly with remove_image_size() in a child theme’s functions.php or a small site plugin, so new uploads stop producing it. Otherwise you clean up today and start manufacturing the same orphans tomorrow.
Originals, scaled copies and the confusing middle
Since WordPress 5.3 there is an extra file in the chain, and it makes regeneration results harder to read than they used to be. On upload, wp_create_image_subsizes() compares the image against the big_image_size_threshold filter, which defaults to 2560 pixels. Anything larger gets scaled down, and the scaled copy becomes what WordPress calls the full size. Core appends -scaled to the filename, so holiday.jpg becomes holiday-scaled.jpg, and the file you actually uploaded is kept alongside it and recorded in metadata as original_image.
A full regeneration reads the true original, not the scaled copy. WP-CLI resolves the source with wp_get_original_image_path(), with one sensible exception: if the attachment has _wp_attachment_backup_sizes, meaning somebody edited it in the built-in image editor, it uses the edited file instead so your edit survives. That is the right choice in both cases, but it has a consequence people trip over. Because the source is the pre-scaled original, the threshold is applied again on every run, and so is EXIF auto-rotation. Change big_image_size_threshold and regenerate, and the full size changes dimensions under you.
The second consequence is that “full size” on a scaled upload is not the file you uploaded. Anyone auditing your largest available image, including you, is measuring holiday-scaled.jpg. When somebody insists the regeneration shrank their originals, this is almost always what they are looking at, and it happened at upload time rather than during the run.
If the source file is gone from disk, which happens after migrations that only copied referenced files, the command warns that it cannot find the attachment, counts it as an error and moves on rather than inventing a source. If the metadata is missing entirely rather than merely incomplete, core takes a different branch and rebuilds everything from the attached file, including a fresh scaled copy, which is why a broken attachment sometimes comes back with different dimensions from its neighbours.
What it costs on a real library
Regeneration is not a metadata operation with some file writing attached. It is a full decode, resample and re-encode, once per size, per image. A 4000 by 3000 pixel JPEG is twelve million pixels, roughly 36 MB of raw bitmap in memory before the image library’s own overhead, and the source has to be decoded again for every size in the list.
The arithmetic is unforgiving. Three thousand images times seven registered sizes is 21,000 encodes. At one second each, which is optimistic for GD on shared hosting with large sources, that is close to six hours. Ten thousand images with ten sizes is an overnight job. This is why the honest answer to “how long will it take” is a range, and why --image_size and a targeted list of attachment IDs are worth the five minutes it takes to work out which images actually need the work.
Memory is the other ceiling. WordPress raises the PHP limit for admin and CLI context through WP_MAX_MEMORY_LIMIT, which defaults to 256M against the 40M that WP_MEMORY_LIMIT defaults to on a single site. A handful of very large images can still exceed it, and when they do the sub-size write fails, the error is swallowed, and the run carries on with a short sizes array.
Running that load through wp-admin during business hours is a bad idea for a reason that has nothing to do with images. Each image is a full PHP request with your entire plugin stack booted, and it occupies a PHP worker for its duration. On a host with four workers, a regeneration eats most of your capacity and every visitor request queues behind it. Do it over SSH, off peak, detached from your terminal.
# Measure before, so you can measure after.
du -sh wp-content/uploads
# Detach the run so a dropped SSH session does not kill it.
nohup wp media regenerate --yes > ~/regen.log 2>&1 &
# Watch progress without holding the process open.
tail -f ~/regen.log
# Same measurement afterwards.
du -sh wp-content/uploads
Plugin route or command line
The plugin route works by queueing attachment IDs in a browser tab and firing one AJAX request per image. For a few dozen images that is fine, and it is the sensible choice if you have no shell access. For anything larger it has three problems: the tab has to stay open for the whole run, one network hiccup or one PHP fatal ends the queue, and every single image pays the cost of a full WordPress bootstrap with every plugin loaded.
The command line loads WordPress once and then loops. It survives a closed laptop if you background it, it writes a log you can grep for warnings afterwards, and it exposes --only-missing, --image_size and --delete-unknown, which most plugins do not offer at all. The rough dividing line is a few hundred images: below that, use whatever is in front of you, above it, use SSH.
If you already run WunderPaint, its media library manager offers thumbnail regeneration as a bulk action alongside the rest of the housekeeping, which is convenient for a folder or a filtered selection, though the shell is still the right tool for a whole library.
Regeneration does not improve quality
This needs saying plainly, because a lot of people run it hoping for sharper images. Regeneration re-encodes from the source at your current settings. If the original is soft, every file it writes is soft, and it will be soft again next time. If your JPEG quality is 82, that is what comes out. Re-encoding a lossy source is neutral at best and marginally worse at worst.
There are two cases where the result does look better, and in neither of them is the encoder improving anything. One is when a registered size did not previously exist, so the browser was stretching a 150 pixel file into a 400 pixel slot. The other is when the existing sub-sizes were written at a lower quality setting than the one in place now. Everything else that reads as blur has a different cause, and the six of them are laid out separately in the guide to why WordPress images look blurry.
Symptom and cause
The uploads folder grew during a cleanup. You used --only-missing or --skip-delete, so the new files were written while the old ones stayed. Their metadata entries were replaced at the same time, which is what makes them unreachable afterwards.
–delete-unknown reports nothing to delete. The metadata no longer lists those sizes, usually because a regeneration already rewrote it. Deletion is driven entirely by the sizes array, so unlisted files are invisible to the command and need a filesystem level audit instead.
Some images report that no regeneration is needed. Nothing differs for them: the recorded sizes match the registered ones and every file is present. On small uploads it can also mean the registered sizes are larger than the source, so image_resize_dimensions() declines to produce them.
The full size got smaller after a run. The upload was over big_image_size_threshold, so the scaled copy is the full size and the threshold is re-applied on every full regeneration. The file you uploaded is still there, recorded as original_image in the attachment metadata.
You changed a size in Settings, Media, and a plugin’s regeneration missed it. Anything built on core’s wp_get_missing_image_subsizes() matches on size name only. WP-CLI compares dimensions as well, so the same job done over the shell picks the change up.
The run stops partway with no useful error. Usually memory on a handful of very large sources, occasionally a file the image library cannot open. Regenerate the failing IDs individually and the log will tell you which one it is.
Where this leaves you
Regenerating thumbnails is a narrow tool with a broad reputation. It fills gaps, it applies changed dimensions, and it re-encodes into a new format. That is the whole list. It does not tidy, it does not optimise, it does not sharpen, and it will not clean up after the theme you just removed unless you tell it to, in the right order, before the metadata that describes those files is overwritten.
The decision is usually quick. Look at what is registered now, look at what the sizes array holds for two or three representative attachments, and if they match, you have your answer without running anything. If they do not, decide whether you care about the files the old set left behind, because that single question determines the order of your commands and whether --only-missing is a shortcut or a trap. Then scope the run as tightly as the situation allows, back up first, and let it go overnight.
Everything in this operation is driven by one serialised array in the database, and once an entry leaves that array the file it named is on its own. Treat the metadata as the map, not the disk, and the whole thing becomes predictable: delete what is listed while it is still listed, write the new set, and audit the folder separately when you have the time to do it carefully.