WordPress Images

WordPress Featured Image Size: The Theme Decides, Not Core

WordPress does not store a featured image size. It stores an attachment ID, and the theme decides which pixels a visitor sees. Here is where that decision lives, what the crop argument does to a portrait, and why registering a size changes nothing for uploads you already have.

WordPress Featured Image Size: The Theme Decides, Not Core

You upload a photograph at 2400 by 1600, set it as the featured image, and the archive page shows a square with the top of someone’s head cut off. The single post shows the same file at a different shape again. Nothing is broken.

The question people ask at that point is what the WordPress featured image size is. It has no answer at the WordPress level, because WordPress does not store a featured image size. It stores one number: the ID of an attachment. Shape, crop and pixels are all decided later, by the theme, at the moment it renders the page.

That is worth knowing before you resize anything, because it tells you where to look. The size lives in the theme’s functions.php or in its template markup. It is not in Settings, Media, and it is not on the post.

Below: where the pointer lives, who picks the crop, why registering a new size changes nothing for what you already uploaded, how to set featured images in bulk without hand editing the database, and a duplication problem this site’s own postmeta table is quietly carrying.

What core stores when you set a featured image

Setting a featured image writes exactly one thing: a row in the postmeta table with the meta key _thumbnail_id and the attachment ID as its value. That is the whole record. No width, no height, no crop, no aspect ratio, no indication of what the image is meant to be used for.

Reading it back goes through get_post_thumbnail_id() in wp-includes/post-thumbnail-template.php, which resolves the post, calls get_post_meta( $post->ID, '_thumbnail_id', true ), casts the result to an integer and runs it through the post_thumbnail_id filter added in WordPress 5.9. has_post_thumbnail() is a thin wrapper on top: it calls the same function and casts the ID to a boolean.

Display happens in the_post_thumbnail( $size, $attr ), whose size argument defaults to the string 'post-thumbnail'. That string is the entire interface between your upload and what a visitor sees. get_the_post_thumbnail() passes it through the post_thumbnail_size filter, then hands the attachment ID and the size to wp_get_attachment_image(), which picks a file from the attachment’s stored sizes array and builds the img tag.

So there are two independent decisions. You decide which picture. The theme decides which pixels of it get shown. Nothing in the editor sidebar touches the second decision, which is why uploading a bigger file so rarely fixes the crop that annoyed you.

Flow diagram of the WordPress featured image path, starting at a single _thumbnail_id row in postmeta, through get_post_thumbnail_id and the size argument of the_post_thumbnail, ending at the image tag built by wp_get_attachment_image.

The theme picks the size, in three lines

A theme opts into featured images with add_theme_support( 'post-thumbnails' ). Passing no argument enables them for every post type. Passing an array, for example add_theme_support( 'post-thumbnails', array( 'post', 'product' ) ), enables them only for those, and current_theme_supports() then checks membership in that array. If a custom post type has no Featured Image panel at all, this is usually why.

Next comes the size itself. set_post_thumbnail_size( $width, $height, $crop ) is not a special function: read it in wp-includes/media.php and it is a single line calling add_image_size( 'post-thumbnail', $width, $height, $crop ). The default featured image size is just a registered image size that happens to be named post-thumbnail.

add_action( 'after_setup_theme', function () {

    add_theme_support( 'post-thumbnails' );

    // The size the_post_thumbnail() uses when nothing else is passed.
    set_post_thumbnail_size( 1200, 675, array( 'center', 'top' ) );

    // A second, wider slot for hero layouts.
    add_image_size( 'site-hero', 1600, 900, true );

} );

If a theme never calls set_post_thumbnail_size(), the name post-thumbnail is never registered. image_downsize() then finds no matching intermediate file and falls back to the real image dimensions, and because image_constrain_size_for_editor() does not recognise the size name either, it applies no constraint at all. That is the case where your 2400 pixel wide upload is served at 2400 pixels wide and squeezed down by CSS, which is slow and often slightly soft.

Block themes complicate this pleasantly. The Post Featured Image block and the Query Loop each carry their own size, chosen in the editor rather than in code, so one theme can render the same attachment at three different sizes on three different templates. The post_thumbnail_size filter is the one lever that reaches the classic template calls wherever they appear, since get_the_post_thumbnail() runs every request through it before anything else happens.

What crop actually does to a portrait photograph

The third argument to add_image_size() is where most of the surprise lives, and the core docblock spells it out precisely.

  • false, the default, scales. The image is fitted inside the box with its proportions intact, so the result usually matches only one of your two numbers. Ask for 1200 by 675 and a 1200 by 1600 portrait comes back at 506 by 675, not 1200 by 675.
  • true crops from the center. You get exactly 1200 by 675, cut out of the middle of the source.
  • An array like array( 'center', 'top' ) crops from a position you choose. The first value is the x crop position and accepts left, center or right. The second is the y crop position and accepts top, center or bottom.

Take that 1200 by 1600 portrait into a 1200 by 675 slot with hard cropping. Core needs a 675 pixel tall band out of 1600 pixels of height, so 925 pixels get discarded. With center cropping it keeps rows 462 to 1137, which on a standing portrait is a band across the torso: no face, no feet. With array( 'center', 'top' ) it keeps rows 0 to 675, which on the same photograph is the head and shoulders. Same file, same registered size, one argument different, completely different picture.

This is also why “just upload it bigger” fails. Cropping discards a fixed proportion of the frame regardless of resolution. A 4000 pixel tall portrait loses the same 58 percent of its height as a 1600 pixel one.

Table comparing three crop settings for add_image_size on a 1200 by 1600 portrait, showing 506 by 675 when scaled, a torso band when cropped from the center, and head and shoulders when cropped from the top, with 58 percent of the height discarded.

Registering a size does nothing to what is already uploaded

add_image_size() writes into the $_wp_additional_image_sizes global and nothing else. It creates no files. The actual cutting happens once, at upload time, when wp_generate_attachment_metadata() walks the registered sizes and writes derivative files plus a sizes array into the attachment’s _wp_attachment_metadata.

So a size you register today exists for uploads from today onward. For everything already in the library, wp_get_attachment_image() asks for a size that is not in that attachment’s sizes array, gets nothing, and falls back to the full file. The layout still looks roughly right because CSS scales it, but you are shipping a 2560 pixel image into an 800 pixel slot, and on some renderings that reads as a blurry image with a cause that has nothing to do with the photograph. Regenerating the library is the step that closes the gap, and the mechanics of one upload becoming many files are worked through in the piece on WordPress image sizes.

One ceiling matters here. Since WordPress 5.3, an upload larger than the big_image_size_threshold value, 2560 pixels by default, gets an extra file with -scaled in its name, and that scaled file becomes what the theme treats as full size. Your 4000 pixel hero shot is not the file being cropped. A 2560 pixel derivative of it is.

Which sizes exist on your site is a question the settings screen cannot answer, because most of them are registered in PHP.

Paste your functions.php below, or just the parts that touch images. The reader pulls out every add_image_size, set_post_thumbnail_size and media option, builds the full table with crop behaviour, and then reports the things that quietly cost you: two sizes with identical dimensions, a size requested by a template that was never registered, a height on an uncropped size that is only a maximum, and the sizes nothing ever asks for.

Registered image sizes

Paste the image size code from your theme, all of functions.php or just the part that registers sizes, and see every size WordPress ends up with, which ones a template actually asks for, and what one upload costs on disk. The PHP is read as text and never executed, and nothing leaves this browser tab.

Or drop a .php file
Drop functions.php here
or press Enter to pick a file. It is read in this tab and never uploaded.
Settings, Media
A typical upload
Nothing read yet
SizeWidthHeightCropSourceRequested
Findings
    Every upload

    Nothing to work out yet.

    File sizes are an estimate: the bytes per pixel of the original are carried over to every derived file. Real files vary with the picture and the quality setting.

    Switch off the sizes nobody asks for
    
      

    This keeps WordPress from writing those files for new uploads. It changes nothing that is already on the server: the old files stay until you regenerate the media library.

    WunderPaint
    The Dynamic Design and Automation Studio
    WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

    A fallback featured image, filtered rather than written

    The common request is a house image for posts that have none, so the archive grid has no holes in it. The tempting solution is a loop that writes _thumbnail_id onto every post that lacks one. Resist it, because get_post_thumbnail_id() already runs its return value through a filter, and a filter is reversible where a database write is not.

    /**
     * Fall back to one house image when a post has no featured image.
     * Nothing is written to the database, so an editor's later choice always wins.
     */
    add_filter( 'post_thumbnail_id', function ( $thumbnail_id, $post ) {
    
        if ( $thumbnail_id ) {
            return $thumbnail_id;
        }
    
        $post = get_post( $post );
    
        if ( ! $post || 'post' !== $post->post_type ) {
            return $thumbnail_id;
        }
    
        return 1234; // Attachment ID of the fallback image.
    
    }, 10, 2 );

    Replace 1234 with a real attachment ID from your own library. The nice part is that has_post_thumbnail() calls get_post_thumbnail_id() internally, so it picks the fallback up for free: templates that guard their markup with if ( has_post_thumbnail() ) start rendering without any change to the template.

    One caveat before you ship it. The REST API fills its featured_media field with get_post_thumbnail_id() too, so the block editor’s Featured Image panel will show the fallback as though someone had chosen it. Wrap the return in if ( ! is_admin() ), or accept it and tell your authors what they are looking at.

    Filtering wins over bulk writing for four concrete reasons. Deleting the code undoes it completely, with no cleanup pass. An author who sets a real featured image later is never fighting a value you already stored. If you retire the fallback attachment, you change one integer instead of hunting for hundreds of rows pointing at a deleted file. And a query for posts without a featured image keeps working, because the meta genuinely is not there.

    The code belongs in a small site specific plugin or in a child theme, never in a parent theme you did not write, because the next theme update overwrites it. The question of where custom code belongs is worth settling once for your whole site rather than per snippet.

    1,314 rows for 114 posts

    Here is what “core stores a single pointer” looks like when it goes wrong quietly. On this site, the postmeta table holds 1,314 rows with the meta key _thumbnail_id. Those rows are spread across 114 posts, an average of eleven to twelve rows each where one would do, and the worst offenders carry sixteen rows apiece.

    Nothing on the site is visibly wrong. Every duplicate row for a given post holds the same attachment ID, and get_post_thumbnail_id() asks for a single value with get_post_meta( $id, '_thumbnail_id', true ), which returns the first entry from the meta cache and stops. The cache is filled by update_meta_cache(), whose query ends in ORDER BY meta_id ASC, so the oldest row always wins and the other fifteen are never looked at.

    The cause is the difference between two functions that look interchangeable. add_post_meta( $id, $key, $value ) defaults its $unique argument to false and inserts a new row every time it runs. update_post_meta( $id, $key, $value ) called without a previous value collects every meta ID matching that post and key, then loops over all of them, and inserts a row only when none exists. Importers, migration scripts and hand rolled loops that reach for add_post_meta stack a fresh row on each pass. Because a later update_post_meta then rewrites all of them to the same value, the duplicates stay perfectly consistent and perfectly invisible.

    The cost is not correctness, it is weight. update_meta_cache() pulls every meta row belonging to the posts in the current loop, so an archive showing twelve posts drags roughly 140 _thumbnail_id rows into memory in order to read twelve integers, and the index on meta_key gets that much less useful across the whole table.

    Look before you touch anything. This query is read only and tells you whether you have the problem and how bad it is:

    SELECT post_id,
           COUNT(*)                   AS rows_for_post,
           COUNT(DISTINCT meta_value) AS distinct_values
    FROM   wp_postmeta
    WHERE  meta_key = '_thumbnail_id'
    GROUP  BY post_id
    HAVING rows_for_post > 1
    ORDER  BY rows_for_post DESC
    LIMIT  50;

    Change wp_postmeta to your own table prefix. The distinct_values column is the one that decides what happens next. Where it reads 1, the duplicates agree and removing them changes nothing a visitor sees. Where it reads 2 or more, two different attachment IDs are stored for one post and the oldest row is currently winning, which means deleting the wrong rows would silently change the picture.

    Statistics and a comparison table showing 1,314 postmeta rows with the key _thumbnail_id spread over 114 posts, up to 16 rows on a single post, and the behaviour difference between add_post_meta and update_post_meta.

    I am deliberately not giving you a delete statement. Deduplicating postmeta is a maintenance job with a backup in front of it and a check of what else writes that key, not something to paste out of a blog post. It fits naturally into a broader pass at cleaning up the media library, where the same instinct applies: inspect, understand the ordering, then delete in a safe order.

    Setting featured images in bulk from the command line

    WP-CLI is the right tool for this, and the first three commands should all be read only. Find out how many posts are affected, confirm what one of them currently points at, and look at the raw rows before assuming there is only one. The --meta_key and --meta_compare flags are passed straight through to WP_Query.

    # Published posts with no featured image at all
    wp post list --post_type=post --post_status=publish 
      --meta_key=_thumbnail_id --meta_compare='NOT EXISTS' 
      --fields=ID,post_title --format=table
    
    # What a single post points at right now
    wp post meta get 123 _thumbnail_id
    
    # Every meta row on that post, duplicates included
    wp post meta list 123 --format=table

    Only once that list looks like what you expected does the write step make sense. Use wp post meta update, never wp post meta add: update rewrites or creates a single row, while add appends one on every run, and re-running the command is exactly how a table ends up with sixteen copies of the same pointer.

    # Point every post that has none at attachment 1234
    wp post list --post_type=post --post_status=publish 
      --meta_key=_thumbnail_id --meta_compare='NOT EXISTS' --format=ids 
      | tr ' ' 'n' 
      | xargs -I % wp post meta update % _thumbnail_id 1234
    
    # After registering a new size, cut the missing files only
    wp media regenerate --only-missing --image_size=site-hero

    Regeneration asks for confirmation before it starts, and it re-encodes files, so run it on a staging copy first if the library is large. If the source files themselves are the problem, and several hundred of them arrive at every proportion there is, WunderPaint’s image processor resizes, converts and re-encodes a whole set in one pass in the browser before any of them becomes a featured image.

    The featured image is a template slot, not a picture

    Everything above is about mechanism. The reason it keeps biting is editorial: people choose featured images as if they were illustrations for one article, when the theme treats them as cells in a grid.

    Choosing a good image per post works beautifully for the first twenty and collapses afterwards, for reasons that have nothing to do with effort. The archive shows twelve at once, and twelve unrelated photographs at twelve different crops, brightness levels and color temperatures look like a mess even when every individual picture is good. Consistency across a grid is a different requirement from quality in isolation, and it is the one that governs how a site feels.

    So settle what a featured image on this site is, as a specification rather than a mood board.

    • One aspect ratio. 16:9 or 1.91:1, chosen because they match how links preview when shared, then never deviated from.
    • One source size. Around 1600 by 900 covers dense screens without pushing past the 2560 pixel scaling threshold.
    • A fixed structure. Where the title sits, if it appears at all. Where the category marker goes. Whether there is a logo.
    • A defined visual field. A photograph, an illustration, a screenshot, a flat color field, but the same kind of thing every time.
    • A rule for legibility. If text sits over an image, there is a scrim or a gradient under it always, not only when the photograph happens to be busy.

    The specification is what makes automation possible, and it is what stops the drift even if you never automate anything.

    Design for the crop you do not control

    Since the theme crops and you cannot predict every template that will ever render the image, treat the center of the frame as the only guaranteed area. On a 1600 by 900 source, a centered 900 by 900 square survives a hard square crop, a 16:9 slot and a 1.91:1 share card. Everything you put outside that square is decoration that may or may not appear.

    Text is where this hurts most. Baked in titles pushed to a corner get sliced in half by the first square thumbnail they meet, and text sized to look right at 1600 pixels wide is unreadable at the 300 pixel width a related posts widget gives it. Keep type large, keep it central, and keep it short.

    Title on the image, or not

    Both work, and they fail differently. A title on the image gives you a card that communicates with no surrounding text, which is exactly the situation when a link is pasted into a chat. It costs you an update problem: change the headline later and the image is wrong, silently, forever. A purely visual image ages better, adapts to any layout and never contradicts the headline, but it says less on its own.

    The deciding question is where traffic comes from. If links get shared a lot, put the title on and accept that the image has to be regenerated whenever the title changes. If most people arrive through the site itself, where the headline already sits next to the image, stay visual and save the maintenance.

    Share previews use a different crop again

    One more actor gets a vote. Most SEO plugins fall back to the featured image when no dedicated social image is set, and they emit the og:image tag pointing at the full or large size of the attachment, not at the theme’s cropped post-thumbnail derivative. The platform then applies its own frame on top, commonly around 1.91:1.

    The result is that the picture people see in a shared link is a different crop of the same file from the one on your archive page, and you chose neither of them. A featured image that the theme squares off nicely can lose its subject entirely in a wide share card, and the reverse happens just as often. Designing the source at 16:9 with the subject centered makes both crops land, which is the practical argument for the specification above.

    While you are there, fill the alt text on featured images. It is the image most likely to be encountered on its own, out of the context of the post, and it is the field most reliably forgotten.

    Symptom to cause

    The archive crops my featured images to a square and cuts off heads. The theme registered post-thumbnail with hard cropping from the center. Change the crop anchor to array( 'center', 'top' ) in a child theme, or pass a different registered size in the template, then regenerate.

    I changed the size in functions.php and nothing happened. add_image_size() only affects uploads made after it runs. The derivative files for existing attachments do not exist yet. Run wp media regenerate --only-missing, or re-upload the handful of images that matter.

    Featured images are sharp on the post and soft on the archive. The archive is asking for a size that is not in the attachment’s sizes array, so core serves the full file and the browser scales it down. Regeneration fixes it, and so does checking that the size the template asks for is actually registered.

    A post shows no image even though a featured image is set. _thumbnail_id points at an attachment that was deleted. get_post_thumbnail_id() still returns a number, has_post_thumbnail() still returns true, and wp_get_attachment_image() returns an empty string. The pointer outlives the file.

    The shared link shows a different framing than the site. The og:image tag points at the uncropped file, not at the theme’s derivative. Set a dedicated social image, or design the source so the center survives both crops.

    The Featured Image panel is missing on a custom post type. Either the post type was registered without thumbnail in its supports array, or the theme called add_theme_support( 'post-thumbnails', array( ... ) ) with a list that does not include it.

    What to do with all of this

    The single fact worth carrying away is that there is no featured image size in WordPress, only an attachment ID and a theme that decides what to do with it. Once that lands, the debugging order writes itself: find the size string the template passes, find where that size is registered, check its crop argument, then check whether the attachment actually has a file at that size. Four steps, and the answer is almost always in the second or the fourth.

    On the production side, the leverage is in deciding the specification before uploading anything. One ratio, one source size, subject centered, text large and central. That single decision removes the crop surprise, the share preview surprise and the archive looks messy problem at once, because all three come from the same root: images made for one context being rendered in three.

    And when you do reach for automation, prefer a filter to a database write and a read only query to a delete. The 1,314 rows sitting in this site’s postmeta table are not causing any visible harm, which is precisely what makes them a good warning: a script that ran once too often, a function chosen without checking its default arguments, and no symptom for years. Look first. It costs one query.

    WordPress Featured Image Size: The Theme Decides, Not Core

    Table of Contents

    Learn it by building something

    Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

    One mail a week, and then it ends.
    Unsubscribe in one click.

    SEO & Structured Data

    Rewrite Rule Tester: What Happens Between the URL and WP_Query

    Between the URL bar and WP_Query sits an ordered array of regular expressions, 247 of them on this blog, stored in a single option row. Paste yours in, walk a request path down them, and see which rule wins, what query it builds and why the 404 happens.

    SEO & Structured Data

    Making a Share Card That Does Not Look Like an Accident

    The tag is rarely the problem. The picture is. Draw a 1200 by 630 card in the page, then work through the crops, the floors and ceilings the platforms enforce, and the design rules that survive being seen on a phone.

    WordPress Images

    Upload Simulator: What WordPress Actually Writes to Disk

    One 3000 by 2000 photo becomes eight files on disk. A 1200 by 800 photo becomes five. Drop your own file in and see exactly which registered sizes fire, which ones core refuses, what dimensions come out and what the whole set weighs.

    CSS & Front-End

    CSS Gradient Generator: Gradients That Do Not Band

    A gradient that looked smooth in the design tool grows stripes on the page. The cause is arithmetic: 8 bit colour, two similar colours and 1600 pixels to cover. How to count the steps before you ship, the three real fixes, and a builder that measures the banding for you.

    Speed & Performance

    A Web Performance Budget for Your WordPress Site

    Pick a connection profile and a target load time, and arithmetic hands you a byte budget per resource class. A web performance budget is a decision tool: it tells you which image, which font and which plugin gets told no, before the page ever gets slow.

    WordPress Images

    Best Image Format for WordPress: WebP, AVIF, JPEG or PNG

    Two thirds of this site's media library is PNG, and nobody decided that. What WordPress actually does with an uploaded file, what its default encoder quality really is, and the three filters that change the format and the size of every sub-size it writes.

    Download the free WunderPaint Plugin for WordPress

    The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

    The Image Editor & Design Studio

    Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

    Free

    Chaos Art

    Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

    Pro

    Particle Strokes

    Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

    Pro

    City Diorama

    Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

    Free

    Papercut Art

    Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

    Pro

    3D Earth Studio

    A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

    Free

    Mystic Studio

    Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

    Free

    Marble Bath

    Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

    Free

    Day Ring

    Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

    Free

    Code Shot

    Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

    Pro

    3D Solar System Studio

    Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

    Pro

    3D Molecule Studio

    Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

    Pro

    3D Textile Studio

    Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

    Pro

    3D Particle Studio

    Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.