The Problem
I upgraded a client marketing site from Next.js 15 to 16 last week. The build passed, the deploy went green, and then half the hero images came back looking soft. A few image requests were failing outright. The server log had this:
Error: Image with src "/hero.jpg" is using quality "90"
which is not configured in images.qualities.
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities
And hitting the optimizer URL directly returned a flat rejection:
GET /_next/image?url=%2Fhero.jpg&w=1920&q=90 -> 400 Bad Request
Nothing in my component code had changed. The <Image> tags still had quality={90} on them like they did in Next 15, where that worked fine. After the upgrade, those exact props were either erroring or getting silently downgraded to a blurrier render.
Why It Happens
Next.js 16 changed the default for images.qualities. In Next 15 and earlier, the image optimizer accepted any quality value from 1 to 100 with no configuration. You could pass quality={90} or quality={100} and it just worked.
In Next 16 the default is [75] and only [75]. If you pass a quality prop that is not in that array, two things can happen. During render and on the optimizer endpoint it logs the unconfigured-qualities error, and a direct request to /_next/image with an unlisted quality returns a 400. When the value is close, Next coerces it to the nearest configured quality, which is why my quality={90} images quietly rendered at 75 and looked soft instead of erroring.
This is a deliberate hardening change, not a bug. The optimizer is a public endpoint. Before this, anyone could request your images at every quality from 1 to 100 and force your server or Vercel's optimizer to generate and cache a hundred variants of every image. That is wasted compute and cache poisoning. Locking the allowed qualities to an explicit allow list shuts that down. The unconfigured qualities docs confirm this is the intended behaviour, and it sits alongside the other Next 16 image config tightening like localPatterns.
The catch is the upgrade codemod does not add your existing quality values for you. If your codebase used anything other than 75, the upgrade silently changes how those images look.
The Fix
Declare every quality value your app actually uses in next.config.ts. First, find them. Grep the codebase for quality props so you do not miss any:
grep -rEo 'quality=\{?[0-9]+\}?' app components --include="*.tsx" | sort -u
That gives you the real list. On the site I was fixing it came back with quality={90} on hero and OG-style images, quality={60} on thumbnails, and the default 75 everywhere else. Add each one to the config:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
// Every quality value used by any <Image> in the app.
qualities: [60, 75, 90],
},
}
export default nextConfig
Restart the dev server after editing the config, because next.config.ts is read at startup and a running process will not pick up the change. Once the array includes 90, the hero images render at full quality again and the 400s stop.
A couple of things worth getting right:
Keep the list tight. Every entry in qualities is a quality the optimizer will generate and cache on demand. The whole point of the change is to limit that surface, so do not list [1, 2, 3, ... 100] to make the error go away. List only the values you genuinely use. Three or four entries is normal.
Pin quality props to listed values. If a component computes quality dynamically, clamp it to the configured set instead of passing arbitrary numbers:
import Image from 'next/image'
const ALLOWED = [60, 75, 90] as const
function nearestQuality(requested: number) {
return ALLOWED.reduce((best, q) =>
Math.abs(q - requested) < Math.abs(best - requested) ? q : best
)
}
export function Thumb({ src }: { src: string }) {
return (
<Image
src={src}
width={320}
height={320}
quality={nearestQuality(70)}
alt="Product thumbnail"
/>
)
}
That keeps every request inside the allow list so you never hit a 400 in production from an off-list value.
Verify against the optimizer directly. The component render is not the only caller. Test the endpoint itself:
curl -s -o /dev/null -w "%{http_code}\n" \
"http://localhost:3000/_next/image?url=%2Fhero.jpg&w=1920&q=90"
A 200 means 90 is configured and serving. A 400 means it is still missing from the array, so re-check the config and that you restarted the server.
The Lesson
Next.js 16 turned images.qualities into a required allow list defaulting to [75], and the upgrade does not backfill the quality values you were already using. Blurry images mean a value got coerced down, and 400s mean it got rejected. Grep for your real quality props, list them in next.config.ts, and clamp any dynamic values to that set.
If a Next.js major upgrade left your site rendering wrong in ways that passed the build, that is the kind of cleanup I do on client projects. See my services. For another Next 16 image config breaking change, read next/image remotePatterns wildcard broken after upgrade.
Upgrading to Next.js 16 and something broke quietly? Let's fix it.
