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.

Upload Simulator: What WordPress Actually Writes to Disk

You drop one 3000 by 2000 photo into the media library. WordPress writes eight files. Six are sizes you never asked for, one is a near copy of the file you just uploaded, and the whole inventory ends up in a serialised array in postmeta that nobody reads by hand.

Give the same install a 1200 by 800 photo and it writes five files. Nothing changed but the source. Two of the registered sizes were bigger than the image, so core skipped them, and the scaling step never ran at all. That is why the question “how many files does an upload actually make” has no fixed answer, and why almost nobody can quote the rate at which their own media library grows.

The mechanism is already written up here, size by size, in every file created from a single upload. This piece is the measurement instead: which of your registered sizes fire for one specific file, what dimensions come out, what the set weighs against the thing you dropped in, and which two settings change that number.

So drop a file in. The simulator below runs core’s own arithmetic, function for function, against a size list you can edit, then draws and encodes each generated size in the page so the byte counts are measured from a real blob rather than estimated. It runs entirely in your browser: the image is never uploaded, and the tool makes no network request.

Upload simulator

Drop one image and see what a WordPress install would really do with it: whether it gets a -scaled copy, which sub-sizes are actually written, what each one costs in bytes, the srcset a browser receives and the file names on disk. Every pixel is decoded and re-encoded in this browser tab, nothing is uploaded anywhere.

Drop one image here or press Enter to pick a file. JPEG, PNG, WebP or GIF.

No image yet. Everything below starts at WordPress core's own defaults and can be edited.

Registered sizes
Name Width Height Crop Remove

A height of 0 means no limit, the way core registers medium_large as 768 by 0. With crop off a size is a bounding box, with crop on it is exact, cut from the position you pick.

Paste the add_image_size() lines from functions.php
Install settings

82 and 86 are core's own defaults, one per format. A threshold of 0 switches the big image scaling off, which is what big_image_size_threshold returning false does.

No image yet

Nothing simulated yet. Drop an image above, or press Try a sample photo to watch a 3000 by 2000 pixel upload go through it.

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.

Nobody knows their own size list

The list comes from three places at once, which is why nobody holds it in their head.

Core contributes six. Three of them are editable at Settings, Media: thumbnail at 150 by 150 with cropping on, medium at 300 by 300, and large at 1024 by 1024. The other three have no interface anywhere: medium_large at 768 wide with no height cap, then 1536x1536 and 2048x2048, named after their own dimensions.

Your theme adds more through add_image_size() and set_post_thumbnail_size(). A block theme often registers nothing; a classic magazine theme can easily register six. Plugins add their own, and WooCommerce is the loud example, since its three product sizes are recalculated from the customiser rather than declared once in code.

All of it is merged by get_intermediate_image_sizes(), and the merged list with dimensions and crop flags comes back from wp_get_registered_image_subsizes(). If you have WP-CLI on the server, one command prints the truth for your install:

wp media image-size

Paste those rows into the size table in the simulator, or paste the raw add_image_size() lines out of your theme’s functions.php and let the paste field parse them. It reads them with a regex and tells you how many lines it could not understand, which is usually the ones built from variables.

The scaled copy, and the file underneath it

Before any sub-size is made, core looks at the upload’s dimensions and compares them against a threshold:

$threshold = (int) apply_filters(
    'big_image_size_threshold', 2560, $imagesize, $file, $attachment_id
);

If either side of the image is over 2560 pixels, core saves a constrained copy with -scaled appended to the name, and that copy becomes the “full” size for everything downstream: the block editor, the srcset, the featured image. Your 3000 by 2000 upload is served as photo-scaled.jpg at 2560 by 1707.

The file you uploaded is not deleted. It stays in the same folder under its original name and is recorded in the attachment metadata as original_image, which is what wp_get_original_image_path() gives back. Deleting the attachment deletes both. This is the detail that surprises people auditing disk usage: the largest file in the uploads folder is often the one nothing on the site ever links to.

There is a second consequence, and it is the one that breaks hand calculation. Sub-sizes are generated from the uploaded file, not from the scaled copy. Core passes the original path straight into _wp_make_subsizes(), which has been the behaviour since 5.3.1 and exists so that a 2048 wide size is not resampled twice. It means a registered size can legitimately come out wider than the full size the editor shows you: register a 3000 wide size, upload a 4000 wide photo, and you get a 3000 pixel file sitting next to a 2560 pixel “full”.

The rule that quietly skips sizes

Every candidate size goes through image_resize_dimensions(), and the first thing that function does after its filter is refuse to upscale. The test is not the one most people assume:

// Stop if the destination size is larger than the original image dimensions.
if ( empty( $dest_h ) ) {
    if ( $orig_w < $dest_w ) { return false; }
} elseif ( empty( $dest_w ) ) {
    if ( $orig_h < $dest_h ) { return false; }
} else {
    if ( $orig_w < $dest_w && $orig_h < $dest_h ) { return false; }
}

For a size with both dimensions set, that is an and, not an or. A 1200 by 800 photo against large at 1024 by 1024 passes, because the width is bigger than 1024 even though the height is not, and you get a 1024 by 683 file. The same photo against 1536x1536 fails on both axes, so no file is written and no entry appears in the metadata. Against medium_large, declared as 768 wide with a height of 0, only the width is tested.

At the other end of the same function there is a second refusal that catches the near misses. If the computed output matches the source within one pixel on both axes, core throws the size away rather than write a duplicate, and the tolerance is deliberate: wp_fuzzy_number_match() defaults to a precision of 1 because rounding produces off by one results all the time. A filter, wp_image_resize_identical_dimensions, exists to force the copy anyway and defaults to false.

Table comparing what WordPress core's default registered image sizes produce from a 3000 by 2000 photo and from a 1200 by 800 photo, showing thumbnail, medium, medium_large and large generated in both cases, 1536x1536 and 2048x2048 skipped for the smaller source, and a scaled copy only for the larger one, with totals of eight files against five.

Checking the dimensions by hand

For a size with cropping off, the output comes from wp_constrain_dimensions(). It builds one ratio per axis, but only for the axes that actually need shrinking, then picks between them: it prefers the larger ratio, the snugger fit, unless using it would overflow the box, in which case it falls back to the smaller. Then it rounds, and bumps a result that lands exactly one pixel short of the maximum back up to the maximum.

3000x2000 into a 1024x1024 box
  width ratio  1024 / 3000 = 0.341333
  height ratio 1024 / 2000 = 0.512
  larger ratio: 3000 * 0.512 = 1536, overflows 1024, rejected
  smaller ratio: 3000 * 0.341333 = 1024
                 2000 * 0.341333 = 682.67, rounds to 683
  result 1024x683

For a cropped size the arithmetic runs the other way. Core takes the requested box as a maximum, computes size_ratio as the larger of the two axis ratios, and divides the output size back by it to get the source rectangle. The thumbnail from that same photo is the clean case: 150 by 150 out of 3000 by 2000 gives a size ratio of 0.075, so the rectangle read from the source is 2000 by 2000, and with a centred crop it starts at x = 500, because floor((3000 - 2000) / 2) is 500.

The nine crop positions never change the output dimensions. They change only that starting offset: left pins x to 0, right pins it to the source width minus the rectangle width, anything else centres it, and the vertical axis works the same way with top and bottom. Switch the crop select in the size table from centre to top left and the file stays 150 by 150 while the rectangle moves to 0, 0. That is worth seeing when a theme’s cropped size keeps decapitating people.

One more ordering detail, since it shows up in the output and looks arbitrary otherwise. Core does not create sub-sizes in the order they were registered. It merges a fixed priority list in front first, so medium, large, thumbnail and medium_large are made before anything else, and everything a theme or plugin added follows. The comment in core explains why: if the process dies halfway through, the sizes an editor is most likely to need already exist.

The srcset that falls out of it

Generating the files is half of it. What a visitor downloads depends on which survive into the srcset, and core drops candidates for three separate reasons. The full mechanism is in how core builds srcset and where it goes wrong; the simulator just shows you which of your sizes lost, and to what.

The first filter is shape. Core scales the full image down to each candidate’s width and checks the resulting height against the candidate’s real height within one pixel. Anything cropped to a different aspect ratio fails that test and is silently excluded, which is why a hard cropped 16 by 9 card size never appears in a srcset built from a 3 by 2 photo. The second is width: max_srcset_image_width defaults to 2048, and any candidate wider than that is dropped unless it happens to be the src itself.

The third is the one nobody expects. Core collects candidates into an array keyed by width, so two registered sizes that resolve to the same pixel width cannot both survive: the later one overwrites the earlier one, and which is “later” depends on the creation order above. The chosen src is then moved to the front of the list, a workaround for an iOS 8 bug that is still in core. The accompanying sizes attribute is far simpler than people assume, just (max-width: 800px) 100vw, 800px for a content width of 800, unless a theme filters it.

What the multiplier costs at library scale

One upload is a curiosity. Four thousand is a budget. At core defaults, that 3000 by 2000 photo leaves eight files behind and roughly 2.7 times its own pixel count on disk. Multiply by a real library and you are looking at 32,000 files, and file count is what hurts long before megabytes do.

It hurts where you do not watch. Backups walk every file, so a nightly job that finished in four minutes now takes twenty. Object storage bills per request as well as per gigabyte. Migration tools time out mid-copy. And the admin listing itself slows down, because every attachment carries a serialised metadata blob that grows with each registered size, which is one of the causes covered in what actually makes a media library slow.

Table of six upload setups for the same 3000 by 2000 photo, showing files written and pixels stored on disk: core defaults at 15.9 megapixels across eight files, threshold lowered to 1600 at 13.3, scaling switched off at 11.5, the 1536 and 2048 sizes unregistered at 11.5, both changes together at 8.9, and resizing to 1600 before upload at 4.5 megapixels.

Two of those rows are worth staring at. Unregistering 1536x1536 and 2048x2048 lands on almost exactly the same disk total as switching scaling off entirely, by two completely different routes, and the version where you resize the photo to 1600 before uploading costs less than a third of the default. That last row is not a setting at all. It is the decision about how big a file should be before it ever reaches WordPress, taken by whoever exports it.

The two settings that actually move the number

The first is the registered list, and it is the bigger lever because it changes the file count, not just the bytes. Sizes you can reach in Settings, Media can be set to 0 to switch them off. The rest need code: remove_image_size() for anything a theme or plugin registered, or the intermediate_image_sizes_advanced filter to unset entries just before they are generated, which is the only way to stop core’s three hidden sizes.

add_filter( 'intermediate_image_sizes_advanced', function ( $sizes ) {
    unset( $sizes['1536x1536'], $sizes['2048x2048'] );
    return $sizes;
} );

Before you unset anything, check nothing calls it. A size that no template requests is dead weight; a size a template does request will fall back to the full image and blow up the page weight you were trying to reduce. And the change only affects future uploads: the files already on disk stay until something removes them, which is a job for a usage audit rather than a delete spree, as in cleaning up a media library without breaking pages.

The second setting is big_image_size_threshold. Lower it and every oversized upload gets a smaller served “full”, which is usually the single largest file in the set. Set it to 0 and no scaled copy is written at all, saving one file per upload but leaving your untouched original as the file the site serves. Be clear about what it does not do: because sub-sizes are cut from the upload rather than from the scaled copy, lowering the threshold to 1600 does not stop a registered 2048 size from being generated at 2048.

Quality is a third dial of a different kind: JPEG defaults to 82 in WP_Image_Editor and WebP to 86, and moving it changes bytes without changing a file count. None of these changes rewrite history. Existing attachments keep their old set until you rebuild them, with the caveats in what regenerating thumbnails fixes and what it leaves behind.

Where the simulator stops

The geometry is exact, ported from core. The bytes are not, and the tool says so. Encoding happens on a browser canvas, not GD or ImageMagick, so JPEG and WebP figures are indicative rather than the exact bytes your server writes. PNG is not comparable at all, since the quality setting does not apply to it. A GIF is measured as a PNG, because a canvas cannot write GIF, and the row says so after reading the type back from the resulting blob.

A canvas also discards colour profiles and EXIF, where a server keeps some of both, so a wide gamut photo reads lighter here than on disk. HEIC and AVIF usually refuse to decode in a browser at all, even though a server would accept them, and HEIC is the one format core converts on upload, to JPEG. The src shown in the srcset panel is modelled the way image_get_intermediate_size() picks one, the smallest generated candidate at least as wide as your content width with the original’s shape, and a theme is free to ask for something else entirely.

The practical limits are small. Only the first file of a multi-file drop is used, non-images are refused with a reason, the size table caps at 24 rows, and anything over 40 megapixels gets a warning rather than a refusal. Encoding runs one size at a time to keep memory bounded, so a big source with a long list takes a moment and reports progress.

The number you were missing

The gap between what you upload and what lands on disk is not a mystery and it is not a bug. It is a set of small, defensible rules: do not upscale, do not duplicate, keep the original, prefer the snug fit, make the important sizes first. Each one is reasonable on its own. Stacked, and multiplied by a size list nobody has read since the theme was installed, they produce a folder several times the weight of the photographs in it.

What changes the conversation is a real number for a real file. Run one representative photo through with your actual size list, then the smallest thing you upload routinely, a logo or a headshot, and watch how many sizes core refuses. The two answers tell you whether your problem is the list, the threshold, or the files people hand you.

Then change one thing and re-run it. That is the honest version of image optimisation: not a plugin that promises a percentage, but a number you measured, a setting you understood before you touched it, and the same number measured again.

Upload Simulator: What WordPress Actually Writes to Disk

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.

Troubleshooting

Why Is Your WordPress Site Slow? A Diagnostic Order, Not a Checklist

WordPress performance advice is usually handed over as a flat, alphabetized checklist. This walks through the same fixes in the order they actually pay off, starting with the thing most sites get wrong first: images.

Security & Privacy

SPF, DKIM and DMARC Builder: Why Your Contact Form Mail Vanishes

Three DNS records decide whether your contact form mail arrives: SPF lists the servers, DKIM signs the message, DMARC ties both to the From address a reader sees. What each one checks, why a passing SPF record can still fail DMARC, and where WordPress mail goes wrong.

Design Fundamentals

Ten Typography Mistakes That Give an Amateur Design Away

Bad type is rarely the wrong font. It is almost always spacing, size and a handful of habits nobody ever told you to drop.

Security & Privacy

Content Security Policy Generator: The Header Nobody Dares Switch On

A Content Security Policy is the header everyone knows they should have and nobody dares enforce. What it actually stops, why a policy with only default-src is either useless or breaks everything, the two keywords that undo the whole thing, and what fails first on a real WordPress site.

Developer Tools

Text Cleaner: Getting the Junk Out of Pasted Text

Pasted text carries more than words: Word markup, PDF line breaks, tracking parameters, and a set of characters that render as nothing at all. What each one actually is, why WordPress makes some of it worse, and what to strip.

Security & Privacy

Black out every piece of text in a screenshot before you share it

A tool that guesses which text is sensitive will miss the one that mattered. This one covers every text field it finds and lets you click back what can stay. It also refuses to default to a blur, because blurring and pixelation can be undone, and there is published work showing exactly how.

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.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.

Pro

Step Guides

Turn any picture into an instruction. Every mark is pinned to a place in the image, so arrows still point at the right thing after the callout has been dragged somewhere else.

Free

Text Art

One studio, sixteen art types: ASCII and emoji art, brick, dice, cube, sticky-note, LED, ceramic and keycap mosaics, word portraits, text flows, silhouettes, element tiles and more.

Free

Photo Mosaic

The classic photomosaic, computed in your browser: your main image emerges from many media-library photos via true structure matching - never a cheap overlay.

Free

Puzzle Sheets

Generate printable puzzle sheets: word search, mazes in eight shapes, sudoku with unique solutions, criss-cross, cryptograms and number pyramids.