If you’re seeing strange 404 errors in your WordPress site logs or in the Redirection plugin, and they’re referencing image files in your /wp-content/uploads/ folder, you’re not alone. I recently encountered exactly that problem — and solving it required a bit of detective work.

Here’s how I tracked down the source of the 404s, figured out why they were happening, and how you can fix them on your own site.

The Symptom: Mysterious Image 404s

I recently migrated a client’s site from SquareSpace and another platform into a unified site, and with that, came along a lot of blog post imports, many originally from Medium. While the front end of the site looked just fine, our 404 log (through the Redirection plugin) was showing a lot of paths.

What struck me as odd was that these URLs seemed to be coming from WordPress’s native image handling — not from broken manual links or theme code. They included an image size, and a comma indicating there was another image after that. This indicates this was coming from the srcset attribute, rather than src.

/wp-content/uploads/2025/05/SeriesB-featured-r1b.png 1200w, https:/stg-testingsite-stage.kinsta.cloud/wp

The Clue: The srcset Attribute

Digging into the HTML markup on the affected pages, I saw that the srcset attribute on image tags was trying to load image sizes that didn’t actually exist on the server.

Where They Were Coming From

While Redirection doesn’t give you the referring page for these 404s, you can find them via the media library. Just type in the name of the image file name:

and you’ll see which post it’s attached to:

You can then look up that post and check out the image it’s referencing in the file:

After some hunting, I found that these broken image sizes were being generated by the default WordPress Image block in Gutenberg. On the post in question, it LOOKED like the editor had selected the image display size as “Thumbnail.” That instructs WordPress to load and serve the thumbnail size (typically 150x150px), which is generated during the upload process.

However, the actual 150×150 image files weren’t there. That means either:

  • They were never generated.
  • They were deleted during a media cleanup or migration.
  • WordPress’s metadata was out of sync with the filesystem.

Looking at the page in code view revealed that there wasn’t a resolution set at all, so the WP UI was a red herring.

<!-- wp:image {"id":2364} -->
<figure class="wp-block-image"><img src="https://website.com/wp-content/uploads/2025/04/c1e0bee11d85fd97a06943c1d01f46dba2b8b24e-1600x500-1-scaled.png" alt="" class="wp-image-2364"/></figure>
<!-- /wp:image -->

If the image truly had a resolution set, it would look like this with the sizeSlug attribute designated:

<!-- wp:image {"id":2363,"sizeSlug":"full"} -->
<figure class="wp-block-image size-full"><img src="https://website.test/wp-content/uploads/2025/04/816944b4bd6d7435217a5fe1a93c23f052d5fc6d-1600x1094-1-scaled.jpg" alt="" class="wp-image-2363"/></figure>
<!-- /wp:image -->

The Root Cause

WordPress stores data about all image sizes in a field called _wp_attachment_metadata, which includes file paths and dimensions for each size. If a file is missing from disk, WordPress still believes it exists and includes it in the srcsetmarkup — causing browsers to try loading it and logging a 404.

Diagnosing It With A PHP Script

Save the following to a php file named image-size-audit.php and then run it in your terminal with wp eval-file path/to/image-size-audit.php.

<?php
/**
 * Image Size Audit – Detect images with **no sizeSlug set**.
 *
 * Scans all published posts for <code>core/image</code> blocks where the
 * <code>sizeSlug</code> attribute is missing or empty.  It then exports the
 * findings to a CSV in <code>wp-content/uploads</code> and writes a concise
 * summary to STDOUT.
 *
 * Usage (run from project root):
 *
 *   wp eval-file path/to/image-size-audit.php
 *
 * Exit codes
 * ----------
 * 0 – No problems (every image has an explicit sizeSlug)
 * 1 – At least one offending image found (CSV generated)
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Safety: run within WP only.
}

/**
 * Main controller.
 */
function audit_images_with_unset_size() {
    $posts = get_posts( [
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
    ] );

    if ( ! $posts ) {
        echo "🚫 No published posts found." . PHP_EOL;
        exit( 0 );
    }

    $results = [];

    foreach ( $posts as $post ) {
        $blocks = function_exists( 'parse_blocks' ) ? parse_blocks( $post->post_content ) : [];
        scan_blocks_for_unset_size( $blocks, $post, $results );
    }

    if ( empty( $results ) ) {
        echo "✅ All images have a sizeSlug set." . PHP_EOL;
        exit( 0 );
    }

    // Write CSV.
    $upload_dir = wp_upload_dir();
    $filename   = 'image_size_unset_audit_' . date( 'Ymd_His' ) . '.csv';
    $filepath   = trailingslashit( $upload_dir['basedir'] ) . $filename;
    wp_mkdir_p( $upload_dir['basedir'] );

    $fh = fopen( $filepath, 'w' );
    fputcsv( $fh, [ 'post_id', 'post_title', 'image_url' ] );
    foreach ( $results as $row ) {
        fputcsv( $fh, $row );
    }
    fclose( $fh );

    $post_count  = count( array_unique( array_column( $results, 0 ) ) );
    $image_count = count( $results );

    printf( "⚠️  %d images across %d posts have NO sizeSlug set." . PHP_EOL, $image_count, $post_count );
    printf( "📄 CSV saved to: %s" . PHP_EOL, $filepath );

    exit( 1 );
}

/**
 * Recursively examine blocks.
 *
 * @param array   $blocks   Parsed Gutenberg blocks.
 * @param WP_Post $post     The post context.
 * @param array   $results  Reference array accumulating CSV rows.
 */
function scan_blocks_for_unset_size( array $blocks, WP_Post $post, array &$results ) {
    foreach ( $blocks as $block ) {
        if ( isset( $block['blockName'] ) && 'core/image' === $block['blockName'] ) {
            $size_is_set = isset( $block['attrs']['sizeSlug'] ) && '' !== $block['attrs']['sizeSlug'];
            if ( ! $size_is_set ) {
                // Get image URL for context.
                $img_url = '';
                if ( ! empty( $block['attrs']['url'] ) ) {
                    $img_url = $block['attrs']['url'];
                } elseif ( ! empty( $block['innerHTML'] ) && preg_match( '/<img[^>]+src="([^"]+)"/i', $block['innerHTML'], $m ) ) {
                    $img_url = $m[1];
                }

                $results[] = [
                    $post->ID,
                    get_the_title( $post ),
                    $img_url,
                ];
            }
        }

        // Recurse into nested blocks.
        if ( ! empty( $block['innerBlocks'] ) ) {
            scan_blocks_for_unset_size( $block['innerBlocks'], $post, $results );
        }
    }
}

// -----------------------------------------------------------------------------
// Kick off the scan.
// -----------------------------------------------------------------------------

audit_images_with_unset_size();

This told me exactly which attachments were referencing a thumbnail that didn’t exist on disk.

The Fix:
1) Regenerating Thumbnails

Once I had the list of affected images, you should use the Regenerate Thumbnails plugin — or what I prefer, WP-CLI:

wp media regenerate --only-missing

This recreated all the expected image sizes for any file that had missing derivatives.

2) For images missing a specific thumbnail resolution value, add “full” to these blocks.


Save the following php script as set-thumbnail-size.php:

<?php
/**
 * Usage (from WP root):
 *   wp eval-file set-image-size-full.php
 *
 * Scans all posts for <core/image> blocks that lack a sizeSlug attr
 * and sets sizeSlug => 'full'.
 */

$args = [
    'post_type'      => 'post',   // change if needed
    'posts_per_page' => -1,
    'post_status'    => 'any',
];

$posts   = get_posts( $args );
$updated = 0;

foreach ( $posts as $post ) {

    $original = $post->post_content;
    $blocks   = parse_blocks( $original );
    $changed  = false;

    // --- recursive walker --------------------------------------------------
    $walker = function ( &$block ) use ( &$walker, &$changed ) {
        if ( $block['blockName'] === 'core/image' ) {
            if ( empty( $block['attrs']['sizeSlug'] ) ) {
                $block['attrs']['sizeSlug'] = 'full';
                $changed = true;
            }
        }

        // Walk innerBlocks (gallery, columns, etc.).
        if ( ! empty( $block['innerBlocks'] ) ) {
            foreach ( $block['innerBlocks'] as &$inner ) {
                $walker( $inner );
            }
        }
    };

    foreach ( $blocks as &$block ) {
        $walker( $block );
    }

    if ( $changed ) {
        $new_content = serialize_blocks( $blocks );
        wp_update_post( [
            'ID'           => $post->ID,
            'post_content' => $new_content,
        ] );
        printf( "Fixed post %d – %s\n", $post->ID, $post->post_title );
        $updated++;
    }
}

printf( "\nDone. %d post(s) updated.\n", $updated );

Run it

I recommend doing this on a dev/staging environment first.

# 1 – backup first
wp db export before-thumbnail-set.sql

# 2 – execute
wp eval-file set-thumbnail-size.php

The script will list every post it altered and finish with a summary like:

Fixed post 231  – Data Ingestion 101
Fixed post 242  – Top Ten Integrations

Done. 2 post(s) updated.

What the script does (and why it’s safe)

  • WordPress native parsers (parse_blocks, serialize_blocks) guarantee you won’t corrupt block markup.
  • It skips any image block that already has a sizeSlug (thumbnail, medium, custom, etc.).
  • Posts are only written when something actually changes, so revisions/history stay clean.
  • wp_update_post() triggers the usual save hooks, thumbnails, caches, etc.

Why this solves the 404 problem

  • Blocks that had sizeSlug:”thumbnail” were instructing browsers to request …-150×150.*.
  • After the update they’ll load the full-size source, and the srcset no longer includes the missing 150 × 150 derivative, so the 404s disappear.

Preventing It in the Future

  • Avoid selecting “Thumbnail” size in Gutenberg unless you’re sure that size exists.
  • If you change your media size settings (in Settings → Media), regenerate thumbnails right after.
  • Watch out for cleanup plugins that remove unused sizes — they may leave your database metadata pointing to missing files.
  • When one is cutting and pasting from another website like Medium into the WordPress editor, before you hit “publish” there will be an option for importing those pasted images into your own media library so you’re not serving them from offsite. At this time, doing so DOES NOT properly select an image resolution size, so after publishing, you’ll need to go back in and do that manually. This is because before the image exists on your own server, WP doesn’t have multiple sizes to reference.

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.