How to Fix: Optimizing images from third party sources does not work after migrating from 14.2.16 to 15.0.1.
Third-party image optimization can silently break after upgrading from Next.js 14.2.16 to 15.0.1 when your existing remote image configuration no longer matches the stricter runtime behavior in the new image pipeline. In most real-world migrations, the failure is not the <Image /> component itself, but a mismatch between the requested remote URL, the configured images.remotePatterns, proxy behavior, headers, redirects, or a custom loader path introduced by a more complex deployment setup.
Understanding the Root Cause
After migrating to Next.js 15.0.1, image optimization from third-party sources may fail because the framework validates remote image requests more strictly than before. In projects that worked on 14.2.16, the configuration often relied on assumptions that no longer hold once the upgraded server starts resolving image URLs through the newer optimization flow.
The most common technical causes are:
- Incomplete remotePatterns matching: the protocol, hostname, port, pathname, or search parameters of the external image URL no longer match exactly what Next.js expects.
- Redirected asset URLs: your source may start at one CDN domain and redirect to another. If only the first hostname is allowlisted, optimization fails when the final URL is checked.
- Custom server or proxy interference: reverse proxies, middleware, or platform routing can alter the
/_next/imagerequest, strip query parameters, or block upstream fetching. - Mixed environment configuration: development and production may resolve different asset hosts, especially when environment variables build the image URL dynamically.
- Changed behavior around local vs remote image handling: some setups accidentally depended on looser matching in prior versions.
In complex applications, this bug is hard to reproduce in a minimal repo because the actual failure usually depends on deployment-specific infrastructure, such as CDN rewrites, authentication headers, custom loaders, monorepo config merging, or environment-based hostnames.
At runtime, the /_next/image endpoint receives a URL like ?url=https%3A%2F%2Fcdn.example.com%2Fimage.jpg&w=1200&q=75. Next.js then verifies whether that remote URL is allowed. If the final resolved request does not satisfy your configured rules, the optimizer rejects it, often returning a 400, 500, or a broken image in the browser.
Step-by-Step Solution
The fix is to verify the exact remote image URL being requested, then align your configuration and infrastructure with the stricter matching rules in Next.js 15.
1. Inspect the failing optimized request
Open the browser network tab and inspect the failing /_next/image request. Decode the url parameter and confirm:
- the exact protocol
- the final hostname
- whether there is a redirect
- the full pathname
- whether the request depends on signed query parameters
If the original image URL is generated in code, log it on the server side before rendering the <Image /> component.
console.log('Resolved image URL:', imageUrl)
2. Replace fragile domain config with explicit remotePatterns
If your config still depends on older broad patterns or incomplete host rules, define precise images.remotePatterns in next.config.js or next.config.mjs.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example-cdn.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'media.example.com',
port: '',
pathname: '/assets/**',
},
],
},
}
module.exports = nextConfig
If your provider redirects from one host to another, add both hostnames.
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'source.example.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.redirect-target.com',
pathname: '/**',
},
],
}
3. Verify that config merging did not drop image settings
In monorepos and plugin-heavy projects, the migration may have changed how config wrappers compose. Validate that the final exported config still contains your images section.
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example-cdn.com',
pathname: '/**',
},
],
},
}
module.exports = withBundleAnalyzer(nextConfig)
If multiple wrappers mutate the config, temporarily log the final object during startup.
console.log(JSON.stringify(nextConfig, null, 2))
4. Confirm that /_next/image is not rewritten or blocked
Check your proxy, CDN, platform routing rules, and middleware. The optimizer endpoint must remain reachable and must preserve its query string.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/_next/image')) {
return NextResponse.next()
}
return NextResponse.next()
}
If you use rewrites, verify they do not capture /_next/image unintentionally.
async rewrites() {
return [
// Avoid broad rewrites that also match /_next/image
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
]
}
5. Test the remote asset directly
Make sure the third-party source actually returns the image without requiring blocked cookies, geo-specific authorization, or unsupported anti-bot checks. Test both the original image URL and the optimizer URL in the deployed environment.
curl -I "https://images.example-cdn.com/path/to/image.jpg"
curl -I "https://your-app.example.com/_next/image?url=https%3A%2F%2Fimages.example-cdn.com%2Fpath%2Fto%2Fimage.jpg&w=1200&q=75"
If the upstream server blocks non-browser requests or requires special headers, Next.js optimization may fail because the server cannot fetch the asset normally.
6. Review custom loaders or custom image paths
If your project uses a custom loader or overrides the image path, verify that the migration did not break the generated URL format.
images: {
loader: 'default',
path: '/_next/image',
}
If you intentionally use a custom loader, make sure it returns valid URLs for all environments.
import Image from 'next/image'
const cdnLoader = ({ src, width, quality }) => {
return `https://images.example-cdn.com${src}?w=${width}&q=${quality || 75}`
}
export default function Avatar() {
return (
<Image
loader={cdnLoader}
src="/avatars/user.jpg"
alt="User avatar"
width={300}
height={300}
/>
)
}
If the issue disappears when using unoptimized, that is a strong signal that the failure is inside the optimization path rather than the source URL itself.
<Image
src={imageUrl}
alt="Preview"
width={1200}
height={800}
unoptimized
/>
Use this only for diagnosis, not as the final fix unless optimization is intentionally disabled.
7. Use a hardened final configuration
For most migrations, the stable fix looks like this:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example-cdn.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'media.example.com',
port: '',
pathname: '/assets/**',
},
],
formats: ['image/avif', 'image/webp'],
},
}
module.exports = nextConfig
Then restart the app, clear build output, and retest in the same environment where the failure originally occurred.
rm -rf .next
npm run build
npm run start
Common Edge Cases
- Signed image URLs expire: optimization fetches the image server-side, so a short-lived token may expire before the request completes or during cache refresh.
- Hostname differs by environment: staging may use
staging-cdn.example.comwhile production usescdn.example.com. Both need matching config if both are used. - Protocol mismatch: the app generates
httpURLs locally andhttpsURLs in production, but only one protocol is allowlisted. - Pathname is too restrictive: using
/images/**will fail if some assets live under/media/**. - Redirect chains: the original URL may be valid, but the final fetch target is a different CDN or storage bucket.
- Middleware authentication: auth middleware may accidentally protect
/_next/image, causing image optimization requests to fail for anonymous users. - Reverse proxy query stripping: if your infrastructure removes
w,q, orurlparameters, the optimizer cannot function. - Third-party hotlink protection: some providers deny server-originated fetches even though direct browser access appears to work.
FAQ
Why did this work in Next.js 14.2.16 but fail in 15.0.1?
Because your project likely depended on behavior that was more permissive or less visible in the older version. After upgrading, remote image validation, infrastructure routing, or config composition exposed a mismatch between the actual remote URL and your configured allowlist.
Should I switch back to images.domains?
Usually no. images.remotePatterns is the safer and more explicit choice because it lets you control protocol and path matching. That precision matters when debugging CDN redirects and environment-specific asset paths.
Can I fix this by setting unoptimized on every image?
You can use unoptimized as a temporary workaround or diagnostic tool, but it bypasses the built-in optimizer. The proper fix is to correct the remotePatterns, proxy behavior, redirects, or loader configuration so optimized delivery works again.
If your migration only broke in a complex setup, treat the issue as an environment-aware image pipeline problem, not just a component bug. In practice, the winning strategy is to trace the exact remote URL, allow every real fetch target explicitly, and ensure nothing in front of /_next/image rewrites, blocks, or mutates the request.