Why WordPress REST-API Uploads Skip Their Responsive Image Sizes

Date:

If you publish images with a script instead of the media library, WordPress may generate only two sizes of each. The page still works, so nothing looks broken — but every visitor downloads your full-size file.

The short answer: themes that register custom image sizes often do so in a context a REST request never loads. Finding none of them registered, the upload falls back to just full and thumbnail, then stops. The fix is to register those sizes on init, which does run for REST.

What responsive image sizes actually do

When you upload an image, WordPress creates smaller copies at registered dimensions. It then writes a srcset attribute listing them, and the browser picks whichever fits the viewport. A phone gets a small file, a desktop gets a large one, and nobody downloads more than they need.

That mechanism has one dependency: the sizes must exist. If only the original and a square thumbnail were created, there is nothing to choose between, and every device is served the original.

Where the sizes go missingThe upload succeeds at every step. Only the generated sizes differ.
1. Upload

Image arrives over the REST API. Succeeds normally.

2. Which sizes?

WordPress asks what is registered — but a REST request never ran the theme's registration.

← the failure point
3. Generate

Creates a copy for each registered size. Only two exist, so it makes two.

4. Render

Template asks for a size that was never made, and falls back to the full file.

No error is raised at any stage — which is why this survives unnoticed until someone measures the page.

The symptom is silent

This failure produces no error. The image appears, the layout is correct, and the media library shows the file. The only visible trace is in the page source, where the <img> tag carries no srcset, or in a network panel, where a phone pulls a file sized for a desktop.

That silence is why it survives. A broken image gets noticed immediately; a heavy one does not, until someone measures the page.

The page looks perfect on every screen; only a network trace shows the phone pulling the full-size file.

Diagnosis: three checks, in order

1. Count the generated sizes. Query the media endpoint for an image you uploaded by script and read media_details.sizes. A healthy upload on a theme with custom sizes returns many entries. A failing one returns two:

full       1600x900
thumbnail  150x150

2. Rule out the file format. Re-upload the identical picture in a different format. If a JPEG behaves exactly like a WebP, the format is not involved. This step matters — it is easy to blame a newer format and start rewriting an encoder, when the encoder was never the problem.

3. Ask what WordPress thinks is registered, inside a REST request. This is the check that resolves it, and it needs a temporary endpoint because there is no built-in way to ask:

add_action( 'rest_api_init', function () {
    register_rest_route( 'diag/v1', '/subsizes', array(
        'methods'             => 'GET',
        'permission_callback' => function () { return current_user_can( 'upload_files' ); },
        'callback'            => function () {
            return array(
                'registered_subsizes' => wp_get_registered_image_subsizes(),
                'intermediate_sizes'  => get_intermediate_image_sizes(),
                'is_rest'             => defined( 'REST_REQUEST' ) && REST_REQUEST,
            );
        },
    ) );
} );
Code language: PHP (php)

On the site in question that returned the core sizes only, with every one of the theme’s custom sizes absent. The theme registers them in a path a REST request does not execute, so at upload time they did not exist to be generated.

The fix

Register the same sizes on init, which runs for REST requests as well as page loads. Re-registering a name the theme also registers is idempotent, so this is additive rather than a patch over the theme:

add_action( 'init', function () {
    add_image_size( 'theme_1068x0', 1068, 0,   false );
    add_image_size( 'theme_696x0',  696,  0,   false );
    add_image_size( 'theme_485x360', 485, 360, true  );
    // ...one line per size the theme expects
}, 20 );
Code language: PHP (php)

Two details decide whether this works.

Use the theme’s exact size names. Templates request sizes by name. A size registered as something else will be generated and then never used, because the theme asks for a name that still does not exist and quietly falls back to the original.

Derive the dimensions rather than guessing. Upload one image through the media library, where the theme’s registration does run, then read back the sizes it produced. Compare each one’s aspect ratio against the source: a size that preserves the ratio was scaled, one that does not was cropped. That gives you both the dimensions and the crop flag without reading the theme’s source.

This isn’t a patch over the theme — it registers the sizes where a REST request will actually look.

Repairing images already uploaded

Registering sizes does not retroactively create files. Anything uploaded before the fix still has two. Because metadata generation lives in an admin include that REST does not load, a regeneration endpoint needs to require it explicitly:

require_once ABSPATH . 'wp-admin/includes/image.php';
$meta = wp_generate_attachment_metadata( $id, get_attached_file( $id ) );
wp_update_attachment_metadata( $id, $meta );
Code language: PHP (php)

That missing require_once is a common stumbling block: called without it, the function simply does not exist, and the failure reads as a fatal error rather than as a missing include.

The result

The same attachment, before and after, with no change to the file itself:

Two panels comparing image sizes generated from the same 1600 by 900 file. Before: only two bars, the original at 1600 pixels wide and a 150 pixel thumbnail, with a wide empty gap between them. After: eleven bars stepping down from 1600 through 1068, 747, 696, 485, 324, 300, 218, 150 and 80 pixels.
The exact widths depend on which sizes your theme registers; the shape of the problem does not.
Sizes generated
Before2 — full, thumbnail
After11 — a ladder from 1600px down to 80px

The srcset now spans roughly 1600 · 1068 · 747 · 696 · 485 · 324 · 300 · 218 · 150 · 80. A phone downloads a few tens of kilobytes where it previously downloaded the full-size file.

Hand-uploaded images look perfect while scripted ones ship heavy, and the manual path is the one you spot-check.

Why this matters more than it sounds

Any automated publishing path uses REST. If you are projecting content from a database, a static site generator, or any script, this affects every image you have ever shipped that way — while images added by hand through the dashboard look perfect. The two paths diverge silently, and the manual one is the path you are most likely to spot-check.

Worth checking on your own site before your next batch: upload one image by script, read back its size count, and see which number you get. It isn’t the only REST write that succeeds without doing anything: Rank Math’s SEO fields also return 200 and save nothing until you register them for the REST API.

The short version

Images uploaded through the REST API often get only full and thumbnail, because the theme that registers your custom sizes never loads during a REST request. Register those sizes on init — which does run for REST — and new uploads get the full ladder again. For images already shipped with the gap, regenerate their thumbnails. The failure is silent (no error, just oversized downloads), so confirm the fix by checking for a srcset on the <img> in the page source.

Frequently Asked Questions

Does this affect every WordPress theme?

No. It affects themes that register custom image sizes outside a hook that runs for REST requests. Themes relying only on the core sizes are unaffected, because those are registered by WordPress itself.

Is this a bug in the REST API?

Not exactly. REST requests load a deliberately lighter context than an admin page. The mismatch comes from where a theme chooses to register its sizes; init runs in both contexts, and registration placed there works everywhere.

Will re-registering a size the theme already registers cause a conflict?

No. Registering the same name with the same dimensions is idempotent. Registering the same name with different dimensions would override it, which is why deriving the real values matters.

Does the image format change the outcome?

No. Testing the identical picture as both JPEG and WebP produced the same two sizes, which is what ruled the format out. Modern formats generate intermediate sizes in their own format when they generate at all.

How do I tell whether a generated size was cropped or scaled?

Compare its aspect ratio to the source image. A size that preserves the ratio was scaled to fit; one that differs was cropped to fill. That is enough to reconstruct the crop flag for each size.

Do I have to re-upload images that were already published?

No. Regenerating the attachment metadata rebuilds the missing sizes from the file already on disk, so the original upload, its URL, and any post referencing it all stay intact.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.