WordPress Is Not Generating Your Thumbnails (And It Will Not Tell You)

Short answer

If the PHP image extension is missing, WordPress does not fail your uploads. It
accepts them, returns success, shows them in the media library, and creates
zero resized copies. No warning appears anywhere. Every image slot on your
site then gets the full-size original, which is a page speed problem you cannot
see by looking. Ask the REST API for media_details.sizes — if that object is
empty, nothing was generated.

The failure that never fails

The last thing I wrote about here was
a WordPress page that died with no error log.
That one at least had the decency to crash. Once I fixed it, I checked what else
my PHP install was missing, and found gd — the extension WordPress uses to
resize images.

I expected to find broken uploads. There were none. Every image I had ever
uploaded was sitting in the media library, rendering correctly, returning 200 —
including the featured images that
my publishing pipeline
generates and uploads on its own, which had been reporting success the entire
time.

That is the part worth slowing down on. A missing image extension does not
produce a broken image. It produces a complete image, delivered at full size
into every slot that asked for a small one. Nothing is red. Nothing is logged.
The only symptom is that your pages are heavier than they should be, which is
precisely the symptom nobody notices on a site with no traffic yet.

How do you check whether WordPress made thumbnails?

WordPress records what it generated in the attachment metadata, and the REST API
will hand it to you:

curl -s "https://example.com/wp-json/wp/v2/media/123" \
  | jq '.media_details.sizes | keys'

A healthy upload answers with something like this:

["full", "large", "medium", "medium_large", "thumbnail"]

A broken one answers [], or omits sizes entirely. That empty array is the
whole diagnosis. There is no error to find because no error was ever raised —
the only evidence is the absence of work.

Note what the question actually is. Not did the upload succeed — it did — but
what did it produce. Asking for the artefact instead of the status is the check
I now run on anything that reports its own success.

One false alarm to avoid. A short list is not the same as an empty one.
WordPress skips any registered size that is larger than the source image, so a
716-pixel-wide upload legitimately has no large (1024px) and no medium_large
(768px). On my own site one attachment lists only thumbnail, medium and
full, and it is perfectly healthy. Judge the empty case, not the short one.

Auditing the whole library in one command

Checking attachments one at a time is fine when you are testing a theory. To
answer “how much of my library is affected,” ask for the lot and filter to the
empty ones:

curl -s "https://example.com/wp-json/wp/v2/media?per_page=100&orderby=id&order=asc" \
  | jq -r '.[] | select((.media_details.sizes // {}) | length == 0)
           | "\(.id)\t\(.date[0:10])\t\(.source_url)"'

Anything this prints was uploaded while image processing was unavailable. On my
site it printed exactly one line, and that one line is the rest of this post.

Two notes on running it. The media endpoint is public for attachments attached to
published posts, but authenticate if you want the complete library — orphaned and
draft-attached uploads are exactly the ones nobody thinks to check. And
per_page caps at 100, so a larger library needs paging.

The evidence that outlived the bug

Installing the extension fixed new uploads immediately. What it did not do was go
back in time, and that is where this gets interesting, because one artefact from
the broken window survived in my page source for days afterwards without my
noticing.

The site icon. Here is what WordPress was putting in <head>:

<link rel="icon" href=".../MetaVidaTechInsights-Favicon.png" sizes="32x32" />
<link rel="icon" href=".../MetaVidaTechInsights-Favicon.png" sizes="192x192" />
<link rel="apple-touch-icon" href=".../MetaVidaTechInsights-Favicon.png" />

Look at the sizes attributes, then look at the filenames. WordPress was
declaring a 32-pixel icon, a 192-pixel icon and an Apple touch icon — and
pointing all three at the identical file. Normally there would be -32x32.png
and -192x192.png variants sitting next to the original. They did not exist,
because the icon was uploaded while gd was missing, so nothing was ever
generated.

The markup was not lying, exactly. WordPress asked for a 32-pixel crop, did not
get one, and fell back to the only file it had. So every visitor’s browser was
downloading a 45,975-byte PNG to paint a favicon roughly the size of this full
stop.

The trap: fixing the extension does not fix the images

This is the part that will cost you if you skip it.

WordPress generates resized copies once, during upload. It is not a runtime
operation, and there is no background job that revisits old attachments. So the
sequence goes:

  1. Extension missing → uploads succeed, zero sizes generated.
  2. You install the extension → new uploads are fine.
  3. Everything you uploaded during the broken window is still broken, and now
    it looks fine because your recent uploads look fine.

Step 3 is the trap. You verify the fix on a fresh upload, see five sizes, and
conclude you are done. Meanwhile every image from the broken window keeps
shipping full size forever.

Four things that did not fix it

I had one bad attachment and a whole admin interface full of buttons that looked
like they should fix it. Here is what each one actually did, in the order I tried
them. I checked media_details.sizes after every attempt, which is the only
reason I know any of this.

What I tried What happened
Re-select the image in Settings → General → Site Icon Nothing. The attachment’s modified timestamp did not even change. Selecting an existing attachment reuses it as-is.
Media Library → Edit Image → Scale, set to 32×32 Actively worse. Scale rewrites the master file, so I now had a 32-pixel original being stretched into the 192-pixel and Apple touch icon slots.
Restore original image Recovered the 512-pixel master, thankfully — WordPress keeps a backup. Sizes still empty. Back to square one, literally.
Media Library → Edit Image → Crop Generated the standard sizes, but none of the icon sizes, and my drag shaved the master to 500×512 — no longer square, which matters for an icon.

Four different buttons, four different behaviours, and nothing in their names or
their placement tells you which is which. Two of them are on the same screen.

What actually worked

Uploading the file again. Not re-selecting it, not editing it — uploading it, as
a new attachment, through the Site Icon screen’s upload tab.

That is not a coincidence, and it is the point of this whole post: generation
happens at upload, and only at upload.
Every failed attempt above failed for
exactly that reason. Nothing in the admin interface offers to redo the step that
was skipped, because from WordPress’s point of view the step was never skipped —
it ran, at upload time, and produced nothing.

The before and after, from the same <head>:


<link rel="icon" href=".../Favicon.png" sizes="32x32" />
<link rel="icon" href=".../Favicon.png" sizes="192x192" />
<link rel="apple-touch-icon" href=".../Favicon.png" />


<link rel="icon" href=".../Favicon-512-150x150.png" sizes="32x32" />
<link rel="icon" href=".../Favicon-512-300x300.png" sizes="192x192" />
<link rel="apple-touch-icon" href=".../Favicon-512-300x300.png" />

For ordinary attachments — not the site icon — the same principle applies, but
you have a better tool than re-uploading by hand:

wp media regenerate --yes

That is the WP-CLI route and it is what you want on a library of any size. Mine
had exactly one bad attachment, so re-uploading it was the shorter path.

One warning, since I learned it the expensive way: do not reach for Edit Image
→ Scale.
It is the button whose label most resembles what you want, it asks for
pixel dimensions, and it permanently resizes your original.

Whichever route you take, verify by reading media_details.sizes back
afterwards. Do not verify by looking at the page, because the page looked fine
the entire time it was wrong.

Why this is worth an afternoon

Serving a 1200-pixel image into a 300-pixel slot wastes roughly sixteen times the
pixels, and the browser cannot help you: without the generated sizes, WordPress
emits no srcset, so there is nothing for the browser to choose from. Every
visitor on a phone downloads the desktop-sized file.

Page weight is a ranking input and a bounce-rate input. Worse, this particular
failure scales with your success — the more images you publish while it is
broken, the larger the cleanup, and the cleanup is manual.

FAQ

How do I know if WordPress is generating thumbnails?

Ask the REST API for the media item and look at media_details.sizes. A healthy
upload lists thumbnail, medium, medium_large and large alongside full.
An empty sizes object means WordPress created no resized versions at all, and
it will not have warned you.

Does installing the GD extension fix images I already uploaded?

No. WordPress generates the resized copies once, at upload time. Installing the
extension afterwards only fixes future uploads. Anything uploaded during the
broken window keeps serving the full-size original until you regenerate it or
upload it again.

Why does my image only have two or three sizes instead of five?

That is usually correct. WordPress skips any size larger than the source image,
so a 716-pixel-wide upload will never get the 1024-pixel large size. An empty
list is the real problem, not a short one.

Wrapping up

I have now written two posts about the same afternoon. The first was about an
error that was invisible because nothing would show it to me. This one is about a
failure that was invisible because it never produced an error in the first place.

The second kind is worse, and it is more common than it should be, because
everything in the stack is designed to report outcomes rather than intentions.
The upload succeeded. That was true. It just was not the question I needed
answered.

It is also why the cleanup took five attempts instead of one. When the only
signal is “no error,” you cannot tell a fix that worked from a fix that did
nothing — they look identical from the outside. Four times I clicked something
reasonable, and four times the page kept looking exactly as fine as it had looked
while it was broken.

If you run your own WordPress, go and read media_details.sizes on your oldest
image. It takes thirty seconds, and either you learn something or you get to stop
worrying about it.

This was one of five failures on this site that reported success. The rest, and
the single habit that catches all of them,
are in their own post.

Primary sources:
PHP GD
· Responsive images in WordPress
· wp_get_attachment_metadata
· Serve responsive images