Troubleshooting

Why Your WordPress Images Look Blurry, and Which Cause It Actually Is

Blurry WordPress images are not one problem but six, and each leaves a different fingerprint. Work out which symptom you have before you change anything, because most of the fixes do nothing for most of the causes.

Why Your WordPress Images Look Blurry, and Which Cause It Actually Is

A client sends a photo for the homepage hero. It goes up, and on the live site it looks like it was printed on damp newspaper. The obvious move is to re-upload it at higher quality, which changes nothing. The next move is an optimisation plugin, which also changes nothing, and now there are two problems.

Blurry is not one fault. It is at least six, with six different fixes, and each one leaves a different fingerprint. One gets worse as you widen the browser window. One only shows up on a phone. One follows a particular picture around the site but only on certain templates. One looks identical at every size and falls apart in flat areas of colour.

So this is a diagnostic rather than a list of tips. Find the symptom that matches what you are actually looking at, then apply the one fix that goes with it.

Table matching six causes of blurry WordPress images to the symptom each one produces and the fix that actually applies to it

The original is smaller than the space it fills

This is the cause behind most blurry WordPress images, and it is the one people least expect, because the file looked fine on their own screen before they uploaded it.

WordPress never invents pixels. When it builds the sub-sizes for an upload it runs each registered size through image_resize_dimensions(), and that function bails out early under a comment that states the rule outright: stop if the destination size is larger than the original image dimensions. Register a size at 1200px wide, upload an 800px wide file, and the 1200px version is simply never created. Core will not upscale for you, which is the right call, since upscaling only produces a larger blurry file.

So a 600px wide upload dropped into a 1200px content column has exactly one option. The browser stretches 600 real pixels across 1200 CSS pixels, painting each one about twice and interpolating the difference. There is no better file to fall back to, because no better file exists. That is the softness you are looking at.

The cropped variant of this is sneakier, because it produces a file rather than nothing. When a size is registered with cropping on, core clamps the target to the original instead of refusing, using min() on each side. Register a size called card at 800×600 cropped and upload a 700×1200 portrait, and you do not get an error and you do not get nothing. You get a 700×600 file saved in the attachment metadata under the name card. Templates ask for card, WordPress finds card, serves it happily, and the layout quietly receives 700 pixels where it wanted 800. If you are unclear on how one upload becomes eight or more files in the first place, the way WordPress generates image sizes is worth twenty minutes.

The distinguishing symptom: the image is soft everywhere, on every device, at every screen width, and it gets visibly worse the wider you make the browser window. Drag the window narrow until the image is displayed at roughly its real pixel width and it snaps into focus. If that happens, you have found your cause and nothing else in this article applies. The only fix is a bigger source file.

Comparison showing WordPress refusing to create a scaled size larger than the original, but silently clamping a cropped size to smaller dimensions under the registered name

Sharp on your monitor, soft on your phone

The second cause is the same arithmetic with a multiplier attached. A CSS pixel is a unit of layout, not a unit of hardware. A modern phone packs three device pixels into each CSS pixel, most current laptops pack two, and a ten year old office monitor packs one. An image sitting in a 400px wide slot needs 400 real pixels on the old monitor and 1200 on the phone.

WordPress handles this with srcset, built by wp_calculate_image_srcset() from every generated size that matches the aspect ratio of the requested one, plus the full size file itself. Core even registers two sizes purely to feed it: 1536x1536 and 2048x2048, added since 5.3 as the 2x versions of medium_large and large. There is also a ceiling. Candidates wider than the max_srcset_image_width filter, which defaults to 2048, are dropped from the list unless that file happens to be the src itself.

All of which works, and none of which helps if the candidates were never generated. Those 2x sizes only exist when the upload was big enough to produce them, and the same is true of every other registered size. If the widest candidate for an attachment is 1024 pixels because the original was 1024 pixels, a 3x phone rendering it at 400 CSS pixels gets 1024 pixels for a 1200 pixel job, and any text or fine texture inside the picture goes furry.

The distinguishing symptom: crisp on a desktop monitor, soft on a phone or a recent laptop, and no amount of staring at your desk setup reproduces it. Open the console on the device that shows the problem and type window.devicePixelRatio. If it returns 2 or 3 and your desk monitor returns 1, that is the whole story. How device pixel ratio and srcset interact covers the sizing maths in more depth.

Sharp on one template, soft on another

If the same photograph is crisp on the archive page and soft on the single post, nothing is wrong with the file. A template is asking for the wrong size.

When something calls wp_get_attachment_image() or the_post_thumbnail() with a size name, the request travels through image_downsize() to image_get_intermediate_size(). If that name exists in the attachment metadata, WordPress uses it, however small it happens to be. If the name does not exist, image_downsize() falls through to a branch commented “any other type: use the real image” and hands back the full size instead. Note the inversion, because it is genuinely useful: a size that is missing usually produces an image that is too big. A size that exists but was registered too small for the box it fills is what produces blur.

There is a second trap here that catches people after a redesign. When WordPress decides which sub-sizes to build, _wp_make_subsizes() skips any size whose name already appears in the attachment metadata, and the code comment is explicit that it checks only the name so that existing images are never overwritten even when the dimensions no longer match. Change card from 400×300 to 900×600 in your theme and old attachments keep their 400×300 file under that name forever. New uploads get 900×600. Your library is now half one thing and half the other, and only the older half looks bad.

To settle it, look at what actually exists for one specific attachment rather than at what the theme claims to register.

# Every sub-size that really exists for attachment 123, with dimensions
wp eval 'foreach ( wp_get_attachment_metadata( 123 )["sizes"] ?? [] as $name => $s ) { printf( "%-18s %5dx%-5d %sn", $name, $s["width"], $s["height"], $s["file"] ); }'

# Every size the site currently registers, for comparison
wp media image-size

The first command prints only sizes that were actually generated, which is the number that matters. The second prints what the theme and plugins asked for. Any name in the second list that is missing from the first was skipped because the original was too small.

No WP-CLI on the host? Open the uploads folder for the month the file was added, for example wp-content/uploads/2026/08/, and read the filenames. WordPress writes the dimensions into them, so photo-768x512.jpg tells you everything without a database query.

One more detail worth knowing: when a template passes an array such as array( 800, 600 ) instead of a name, core collects every existing size that is at least that big and matches the ratio, then takes the smallest of them. Array requests degrade gracefully. Named requests do not.

Right dimensions, wrong quality setting

WordPress does not copy your file when it makes a sub-size. It decodes it, resizes it, and re-encodes it from scratch. That re-encode has a quality setting, and WP_Image_Editor::get_default_quality() returns 86 for WebP and 82 for JPEG and everything else.

82 is a sensible default for a photograph of a person outdoors. It is a poor default for a screenshot, a logo on white, a product shot with a hard edge against a plain background, a chart, or anything containing rendered text. Those are exactly the images most business sites are full of, and they are exactly the images where JPEG artefacts are visible.

Two filters control the number. wp_editor_set_quality arrived in 3.5 and applies to every mime type, receiving the mime type and, since 6.8, the target dimensions. jpeg_quality has been there since 2.5 and runs afterwards on JPEG output only, so it has the last word for JPEGs. Both are skipped entirely when the calling code sets a quality explicitly. This belongs in a small site-specific plugin or your child theme’s functions file.

// Raise JPEG quality above the core default of 82.
add_filter( 'jpeg_quality', function ( $quality ) {
	return 90;
} );

// WebP defaults to 86. Raise that too, and leave other formats alone.
add_filter( 'wp_editor_set_quality', function ( $quality, $mime_type ) {
	if ( 'image/webp' === $mime_type ) {
		return 90;
	}

	return $quality;
}, 10, 2 );

Existing files are unaffected. Quality applies when a sub-size is written, so you need to regenerate before the change means anything.

Two caveats. First, your uploaded original is left alone unless its width or its height is above 2560 pixels, the default value of the big_image_size_threshold filter, in which case core makes a re-encoded -scaled copy and that copy becomes the full size that themes serve. Second, WordPress may not be the thing compressing your images at all. Optimisation plugins, a CDN with automatic image processing, and the phone the photo came off all get a turn before you do. Switch the plugin off for one test upload and compare.

The distinguishing symptom: soft in fine detail, blocky mush in areas of flat colour such as sky or a painted wall, and fuzzy halos hugging high contrast edges. It looks the same at every display size, because the damage is baked into the file. The other tell is weight. A 1600px wide photograph that weighs 90KB has had its detail thrown away, whatever the dimensions say. Choosing the right format and quality setting is worth reading before you raise the number blindly.

Nothing sharpened the image after the downscale

Every downscale loses acuity. That is not a bug in any particular tool, it is what happens when you throw away three quarters of the samples in an image and average what remains. Print and photography workflows have sharpened after resizing for decades for precisely this reason.

Core does a small amount of this, in one path only. On hosts with Imagick, WP_Image_Editor_Imagick::thumbnail_image() resizes using FILTER_TRIANGLE, then checks the mime type of the file it loaded and applies unsharpMaskImage( 0.25, 0.25, 8, 0.065 ) only when that file is a JPEG. Resize a PNG or a WebP and no sharpening runs at all. On hosts falling back to GD, resizing is a plain imagecopyresampled() call and there is no sharpening step under any circumstances.

That unsharp pass is deliberately gentle, chosen so it can never make anything look crunchy. It also explains why the same site can look slightly different on two hosts: wp_image_editors lists WP_Image_Editor_Imagick before WP_Image_Editor_GD, so core uses Imagick where it exists and quietly drops to GD where it does not. If your PNG screenshots look softer than your JPEG photographs at the same size, that asymmetry is the reason.

The distinguishing symptom: everything measures correctly. The natural size matches or exceeds the rendered size, the file weight looks healthy, the device pixel ratio is accounted for, and the picture still looks like it is sitting behind a very thin sheet of tracing paper. The fix is upstream. Sharpen at the final output size before uploading, or upload at twice the display size so the browser’s own downscale hides the softness.

Fractional scaling in CSS

The last cause is the smallest and the most annoying to spot. An 800px wide image sits in a box that computes to 793.6px because of a percentage width plus a grid gap plus a border. The browser now has to resample at a ratio of 0.992, and for the common path it reaches for a fast filter rather than a good one.

Small fractional downscales are the worst case there is. At 0.5 the browser can cleanly discard alternate pixels. At 0.992 it blends nearly every pixel with its neighbour a little without ever removing one outright, which smears thin lines and leaves diagonal edges looking unstable. The same thing happens on hover cards that use transform: scale(1.05), which upsamples the image for the duration of the animation, and at any browser zoom level other than 100 per cent.

The distinguishing symptom: a faint shimmer on fine lines that changes as you drag the window, clean at some widths and soft at others, and images with hard geometry (logos, screenshots, diagrams) looking noticeably worse than photographs in the same layout. Give the image enough pixels that the resample has room to spare and the problem stops being visible.

How to check instead of guessing

Five minutes of measuring beats an afternoon of re-uploading. Do these in order and stop when one of them answers the question.

  1. Open the image in its own tab. Right click, open image in new tab, then read the URL. A filename ending -768x512.jpg tells you exactly which registered size the page chose. A filename ending -scaled.jpg means the upload crossed the 2560px threshold and you are looking at core’s scaled copy. No suffix at all means you are getting the full file.
  2. Compare natural size to rendered size. This is the fastest single test there is. Inspect the image, then hover the <img> element in the Elements or Inspector panel. Chrome and Firefox both show a tooltip carrying the intrinsic or natural dimensions alongside the rendered ones. Natural smaller than rendered means you are upscaling, and the ratio tells you how badly. Natural equal to rendered means it is not a sizing problem at all, so go and look at compression and sharpening.
  3. Multiply by the pixel ratio. In the console, window.devicePixelRatio. Rendered width multiplied by that number is how many real pixels the screen wants. Compare it to the natural width from step two.
  4. Check at exactly 100 per cent zoom, on the device that shows the problem. Ctrl or Cmd plus 0 resets it. A browser sitting at 110 per cent resamples everything on the page and will make a perfectly good image look soft.
  5. Compare against the source file on your own disk, viewed at the same pixel width. If your copy looks better, WordPress or a plugin damaged it on the way through. If both look equally soft, the source was never sharp and no setting on the server will fix that.
Four diagnostic steps for a blurry WordPress image, from opening the delivered file to comparing its natural size against its rendered size

Checking properly means measuring, and the measurements are not hard, they are just tedious by hand. The tool below does all four at once.

Drop the image, tell it how wide the picture is displayed and what pixel density you are looking at, and it works out whether the file is simply too small, whether it was upscaled before it was uploaded, whether the compression has eaten the detail, or whether the shot was soft to begin with. There is a magnifier for the part where the answer is usually visible once you know what to look for.

Blurry image diagnosis

Four different faults make a picture look blurry on a WordPress page, and on screen they all look the same. Drop an image in and this measures which one it is. The file is read in this browser tab only: nothing is uploaded and nothing leaves your computer.

Image
Drop an image here
or press Enter to pick one. It is read on your own machine and never sent anywhere.

Sample image, drawn in this browser.

A picture shown 900 CSS pixels wide on a 2x screen needs 1800 real pixels.

Read the display width off the page, not off the file. On a phone that same picture is often only 360 CSS pixels wide.

Nothing analysed yet
Waiting

No image has been read yet.

MeasurementValueThresholdVerdict
Whole image

Loupe at 0, 0

Pixels at 4x, no smoothing
The same crop, edges picked out. Fat pale edges mean the detail is not real.

The thresholds sit in the table so you can disagree with them. Real detail combines the variance of a Laplace filter over the grey values with the share of edges that take more than two pixels to finish. Block edges compare the steps across the 8 by 8 grid that JPEG works in against the steps inside those blocks. Nothing here is a guess about the camera, it is all read off the pixels.

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.

Not having the problem next time

Upload wider than the widest slot the image will ever occupy, then double it for dense screens. In practice that means roughly 2400 to 2560 pixels for a full width hero, around 1600 for in-content images, and 800 to 1000 for cards and thumbnails. Staying at or just under 2560 on both sides keeps you under the default big image threshold, so core does not produce a scaled copy you did not ask for.

Learn what your theme actually registers. Run wp media image-size and read the list against your layout. Any registered size that is narrower than the box it fills is a blurry image waiting for the right upload to come along, and you will find one or two on almost every commercial theme.

Regenerate after you change sizes, always. Because _wp_make_subsizes() matches on the size name rather than the dimensions, editing a registered size does nothing whatsoever for images already in the library. wp media regenerate rebuilds them from the stored original. If the originals themselves need resizing or re-cropping in bulk rather than just regenerating, that is the job WunderPaint’s batch image processor was built for.

Then stop, because the overcorrection has its own cost. A library of 6000px originals eats storage, slows every backup, and pushes image processing into the memory ceilings that make uploads fail outright. Images are usually the first place to look when a WordPress site is slow, and a page that ships four times more pixels than the screen can display is being slow for no visual benefit at all.

Symptom to cause

Soft at every screen size, worse when the window is wide. The original is smaller than the space it is displayed in. WordPress never generates a size larger than what you uploaded, so there is nothing better for the browser to reach for. Re-upload a bigger file.

Sharp on the desktop, soft on the phone. The file has enough CSS pixels but not enough device pixels. A 2x or 3x screen needs two or three times the numbers your layout quotes, and the srcset has no candidate that large because the upload was never big enough to make one.

Sharp on one template, soft on another. The soft template is requesting a registered size that is smaller than the box it fills. Check which sizes actually exist for that one attachment and compare them against the rendered width.

Only images uploaded before the redesign are soft. Someone changed the dimensions of an existing registered size. Core skips regenerating a size whose name is already in the metadata, so old attachments kept their old, smaller files. Regenerate.

Mush in flat areas, halos along hard edges. Over-compression. The dimensions are fine and the encoder discarded the detail, either at core’s default quality or at the hands of an optimisation plugin. Suspiciously small file sizes confirm it.

Everything measures correctly and it still looks veiled. No sharpening after the downscale. Core’s unsharp pass runs only on JPEG files under Imagick, so PNG and WebP sources get none of it and GD hosts get none at all. Sharpen before uploading, or upload at 2x.

A faint shimmer that shifts as you drag the window. The browser is resampling at a fractional ratio with a fast filter. Give the image more pixels than the box needs, and check that no transform or zoom is scaling it during the moment you are judging it.

What this comes down to

Almost every blurry image on a WordPress site is a pixel budget that does not balance. The layout asks for a number of real pixels, something upstream supplies fewer, and the browser papers over the difference. Once you frame it that way the diagnosis is arithmetic rather than intuition: find out how many pixels the slot wants, find out how many the delivered file has, and the gap is your answer.

The two causes that are not about pixel counts, over-compression and missing sharpening, announce themselves clearly once you have ruled out the others. Compression damage lives in flat areas and along edges and never changes with display size. Missing sharpening leaves an image that is correct by every measurement and still slightly veiled.

Start with the devtools comparison between natural and rendered size. It takes about ten seconds, it settles four of the six causes on its own, and it will save you from the very common outcome of installing three plugins to fix a problem that was only ever a 600px file in a 1200px column.

Why Your WordPress Images Look Blurry, and Which Cause It Actually Is

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.

Speed & Performance

Bulk Resize Images in WordPress Without Breaking the Library

Regenerating thumbnails and resizing the original are different jobs, and only one of them is irreversible. What the scaled pair actually costs you, how to measure it with read only commands, what a safe bulk resize has to do step by step, and why most sites should fix the upload threshold instead.

Speed & Performance

Regenerate Thumbnails in WordPress: What It Fixes and What It Leaves Behind

Regenerating thumbnails re-encodes every registered size from the original file. Here is when it is genuinely needed, what each WP-CLI flag really does, why --only-missing quietly keeps the old files, and the order that stops your uploads folder growing.

Troubleshooting

Error Establishing a Database Connection: What WordPress Is Actually Telling You

The message means PHP ran, wp-config.php was read, and the connection to MySQL failed. Nothing more. Here is how to tell a wrong password from a downed server from a host that ran out of connections, and what to do about each.

Speed & Performance

WordPress Lazy Loading Images: What Core Does and When It Hurts

WordPress has added loading="lazy" itself since 5.5, and it skips the first three media elements on purpose, because the top one is usually the largest contentful paint element. Here is the real mechanism: the functions, the threshold of 3, fetchpriority, and the places where the logic never runs.

WordPress Images

WordPress Media Library Folders: Why There Are None, and What Actually Works

WordPress stores every upload as a post, not as a file in a folder, which is why the media library has no directories to organise. Here is what the uploads folder really is, why moving files by hand breaks images everywhere, and how taxonomy-based folders solve it without touching the disk.

Security & Privacy

WordPress File Permissions: 644, 755 and When Something Else

chmod 777 makes the error disappear by handing write access to every process on the server. What the three digits mean, the standard WordPress set, and why ownership decides which digit even gets read.

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.