Next.js 16 Images Broken or Blurry After Upgrade? Fix

Next.js 16 changed next/image defaults: quality is fixed at 75, private IPs are blocked, query strings need localPatterns. Here's each fix.
Next.js 16 changed six image defaults at once. None of them breaks the build, which is what makes them annoying: the site deploys, and then a client emails to say the hero photo looks soft, the logo is gone, or the product images from their own API are all broken. This site runs Next.js 16 too, so these are the settings I check after every upgrade. Here's each symptom with its cause and the config that fixes it.
Key takeaways
- Blurry images:
quality={90}is silently turned into 75. Add the values you use toimages.qualities. - Images from
localhost, a Docker service name or a private IP return 400: setimages.dangerouslyAllowLocalIP, or better, use a public URL. /logo.png?v=2fails: local images with query strings needimages.localPatterns.- New image doesn't show up: optimized images are cached for 4 hours now. Change the file name, not just the file.
- Move from
images.domainstoremotePatterns, and fromprioritytopreload.
Images look blurry or low quality
Before 16, any quality from 1 to 100 was allowed. Now the default list is just [75], and the quality prop is rounded to the closest allowed value. So quality={95} on your hero image quietly becomes 75. No warning. Fix it by listing every quality you actually use:
// next.config.ts
const nextConfig = {
images: {
qualities: [75, 90],
},
};
Keep the list short. Every extra value is another variant the server has to generate and cache, and the limit exists so nobody can ask your server for 100 versions of every image. 75 for most images and 90 for the hero and product photos is enough on almost every site I build.
Images from your own API or storage return 400
This is the one that takes self-hosted sites down. Next.js 16 refuses to optimize images whose host resolves to a private IP address: localhost, 127.0.0.1, 10.x, 172.16-31.x, 192.168.x. It's protection against SSRF, where an attacker uses your image optimizer to fetch things from your internal network.
It hits you if your src points at an internal address, which is common with Docker Compose (http://api:5000/uploads/a.jpg), MinIO or RustFS on the same server, or an API that returns http://localhost:5000/... URLs. The browser gets a 400 from /_next/image, and the image is broken. In dev on your laptop it might still work, because your API is on a public URL there.
The right fix is to give images a public URL (https://cdn.example.com/...) and add it to remotePatterns. If the images genuinely live on a private network you control, you can turn the check off:
images: {
dangerouslyAllowLocalIP: true, // only if you understand the SSRF risk
},
A third option for some cases: if the files are already optimized (they went through an upload pipeline that resizes them), set unoptimized on those images, and the browser loads them directly.
Local images with a query string fail
Cache-busting like <Image src="/logo.png?v=3" /> now needs a localPatterns entry, or Next.js rejects it:
images: {
localPatterns: [
{ pathname: "/assets/**", search: "" }, // no query string allowed
{ pathname: "/logo.png", search: "?v=3" }, // exact query only
],
},
Once you define localPatterns, any local path not listed returns 400. Add every folder you load images from, or you'll fix the logo and break everything else. Easier: drop the query string and rename the file when it changes (logo-v3.png), or import the image statically (import logo from "./logo.png"), which gets a content hash automatically.
"hostname is not configured under images"
Error: Invalid src prop (https://cdn.example.com/a.jpg) on `next/image`, hostname "cdn.example.com" is not configured under images in your `next.config.js`
Not new in 16, but you'll see it when you remove the deprecated images.domains. remotePatterns replaces it, and you can now write it with URL objects:
images: {
remotePatterns: [
new URL("https://cdn.example.com/**"),
{ protocol: "https", hostname: "**.amazonaws.com", pathname: "/my-bucket/**" },
],
},
Keep the pattern as tight as you can. hostname: "**" makes your server an open image proxy for the whole internet, on your bandwidth bill.
You replaced an image and the old one still shows
images.minimumCacheTTL went from 60 seconds to 4 hours (14400). If you upload a new file under the same name and the source doesn't send its own Cache-Control, the old optimized version is served for up to 4 hours. Use a new file name for new content, which is good practice anyway because browsers and CDNs cache too. If an image really has to update in place, lower the TTL:
images: { minimumCacheTTL: 60 },
For a quick manual flush on a self-hosted app, delete .next/cache/images and restart.
Images behind a redirect break
The optimizer now follows at most 3 redirects (it used to be unlimited). Some image hosts and tracking links redirect more than that. Use the final URL as src, or raise images.maximumRedirects if you can't. You can check how many hops an image URL takes with curl -sIL URL | grep -i "^location".
The priority warning
The priority prop is deprecated. Use preload for the image that's your Largest Contentful Paint, usually the hero. Most of the time fetchPriority="high" on that image is all you need. Don't preload more than one or two images, or none of them gets priority. My guide to fixing slow LCP goes deeper into which image to pick.
One small change you probably won't notice: 16 was removed from the default imageSizes. If you use tiny 16 px icons through next/image, add it back or use a plain <img> for icons, which is what I'd do anyway.
Debug any broken image in 30 seconds
Right-click the broken image, open it in a new tab, and read the URL. It looks like /_next/image?url=...&w=1080&q=75. The response tells you which rule failed: a 400 with a message about the URL, a host or local pattern, or a 500 when fetching the source failed. Then check the server's logs (pm2 logs), because the optimizer logs the real reason there.
After-upgrade checklist
- List every
qualityvalue you use inimages.qualities. - Open each page with images from your API or storage on the production server, not just locally.
- Search for
?in local image paths, and addlocalPatternsor rename the files. - Replace
images.domainswith tightremotePatterns. - Swap
priorityforpreloadon the LCP image and run PageSpeed Insights on the homepage.
Frequently asked questions
Why are my images blurry in Next.js 16?
The default allowed quality is now only 75, and other quality values are rounded to it. Add the values you need to images.qualities in next.config, for example [75, 90].
Why do images from localhost fail in Next.js 16?
Next.js 16 blocks image optimization for hosts that resolve to private IP addresses. Serve images from a public URL, or set images.dangerouslyAllowLocalIP: true if the source is on a private network you trust.
Is images.domains removed in Next.js 16?
It's deprecated, not removed. Switch to images.remotePatterns, which also accepts new URL("https://host/**") entries.
How long does Next.js cache optimized images?
At least 4 hours by default since Next.js 16 (minimumCacheTTL: 14400), longer if the source image sends a longer Cache-Control.
Site looks worse after the upgrade?
I upgrade Next.js sites without losing image quality, speed or rankings. Book my Next.js bug fix service, get a technical SEO audit and fix if speed scores dropped, or contact me. Still on middleware.ts? Read how to move to proxy.ts next.
Written by
MD Rakibul Islam Rakib
Full-stack developer, DevOps engineer and Linux system administrator with 5+ years of production experience. I deploy, harden and fix servers and web apps for clients worldwide, and everything in this article runs on real servers I manage, including this site.
- next.js 16 image
- next/image qualities
- dangerouslyAllowLocalIP
- next image localPatterns
- next image blurry


