Writing
Frontend architecture
Mingtindu Sherpa8 min read

Fix Next.js Image 400 Errors in Production

Why next/image requests return 400 Bad Request for remote images, how remotePatterns matching actually works, and how to tell an optimizer problem from an image-hosting problem.

On this page

next/image works locally and then returns a broken image with a 400 Bad Request after deployment. The image itself usually isn't the problem — the Image Optimization API's request to fetch and transform it is being rejected, and the reasons for that rejection are narrower than they first appear.

Two different requests, two different failure points

next/image doesn't just render an <img> tag pointed at your original URL. For anything not marked unoptimized, the browser requests a URL under /_next/image that Next.js's built-in optimizer handles server-side — fetching your original image, transforming it, and returning the result.

That means a broken image can fail at either of two independent points:

  1. The optimizer request (/_next/image?url=...) — rejected by Next.js itself, before it even tries to fetch your image.
  2. The original image URL — reachable in theory, but failing when the optimizer tries to fetch it (wrong content type, requires auth, DNS doesn't resolve from the server, etc.).

Test these separately. Open the original image URL directly in a browser tab, outside of next/image entirely. If that works but the page's image doesn't, the problem is in the optimizer step. If the original URL itself fails or requires authentication, no next/image configuration will fix that — the source needs to be fixed or made publicly fetchable first.

The most common cause: unconfigured remote hosts

By default, next/image will not optimize images from a host it hasn't been explicitly told to trust. Per the Next.js images configuration reference, you allow specific external sources with remotePatterns in next.config.js:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.example.com",
        port: "",
        pathname: "/uploads/**",
      },
    ],
  },
};

Any request whose protocol, hostname, port, or path doesn't match a configured pattern gets a 400 Bad Request from the optimizer — this is documented behavior, not an edge case: "Attempting to optimize any other path will respond with 400 Bad Request error." The same applies to hostname, protocol, and port mismatches for the object form of remotePatterns.

Common ways this mismatch happens:

  • The image host works locally against one environment (a staging CDN, a local upload path) and a different host in production (a production CDN domain) — and only one of them is in remotePatterns.
  • The pattern's pathname is narrower than the real image paths (/uploads/** configured, but images actually live under /media/**).
  • The search field is omitted or set narrowly, and the real image URL carries a query string that doesn't match — Next.js notes that omitting search allows any query string, which is a security tradeoff worth making deliberately, not by accident.
  • port is left empty (meaning "no port in the URL") when the actual image URL includes an explicit, non-default port.

Reading the failing request directly

Open DevTools → Network, find the request to /_next/image?url=...&w=...&q=..., and check:

  • Status code. 400 from the optimizer itself points at remotePatterns (or another optimizer-level rejection) — see the causes below.
  • Response body. Next.js's optimizer error responses describe why the request was rejected; read the actual text rather than assuming.
  • The decoded url parameter. Copy it out and decode it to see exactly which host, path, and query string next/image is trying to fetch — compare that literally against your configured remotePatterns.

Environment differences: why "it works locally" doesn't transfer

A remotePatterns mismatch is often invisible in local development because local setups commonly point at a different image source than production — a local upload directory, a different bucket, or a staging CDN domain. The fix that resolves it locally doesn't fix production because the two environments are legitimately serving images from different hosts. Treat next.config.js's images configuration as environment-specific in your head, even if it's one file: confirm the hosts your production build actually serves images from, not just the ones your local .env points to.

Configuration changes require a restart or redeploy

next.config.js is read at build/start time, not per-request. A remotePatterns change:

  • In local development, requires restarting next dev — it will not take effect on a hot-reloaded page.
  • In production, requires a full rebuild and redeploy. Some hosting platforms cache the previous build's static assets and configuration behind a CDN layer; if a fix doesn't appear to take effect after deploying, check whether the platform needs an explicit cache purge or whether the deployment actually completed with the new next.config.js, not a cached prior build.

Other causes of a 400 that aren't remotePatterns

remotePatterns is the most common cause, but not the only one:

  • Incorrect content type. The optimizer inspects the fetched image's Content-Type. A URL that returns HTML (an error page, a login redirect) instead of actual image bytes — often because the resource requires authentication the optimizer's server-side fetch doesn't have — will fail even if the host is correctly allowlisted.
  • Authentication-protected images. If the original image URL requires a cookie, signed token, or session that only the browser holds, the optimizer's server-side fetch (which doesn't carry the browser's session) will be denied by the origin server, independent of remotePatterns.
  • Redirects from the image host. Next.js follows redirects from an already-allowed remote source without re-validating remotePatterns against the redirect target, but excessive or unexpected redirect chains can still fail depending on maximumRedirects configuration.
  • Quality or format values outside the configured set. If qualities or formats are restricted in images config and a request asks for a value outside that set, the optimizer also returns 400.

Do not assume remotePatterns is the fix for every next/image error — confirm which of these you're actually looking at from the response body before changing configuration.

Fixing it, step by step

  1. Copy the failing /_next/image request's url parameter and decode it.
  2. Open that decoded URL directly in a new browser tab, without going through next/image at all.
  3. If it fails there too, fix the image source first — hosting, permissions, or content type — before touching next.config.js.
  4. If it loads fine directly, compare its protocol, hostname, port, and path against every entry in remotePatterns.
  5. Add or correct the matching pattern, being as specific as the real usage allows rather than defaulting to a broad wildcard.
  6. Restart (next dev) or fully rebuild and redeploy (production) — a config-only save is not enough.
  7. Reload the page and re-inspect the /_next/image request's status code.

Common mistakes

Using domains instead of remotePatterns

The domains config option is deprecated in favor of remotePatterns (see the Next.js images configuration reference) because it can't restrict protocol, port, or path — only hostname. A project still using domains for a new image source should migrate that entry to remotePatterns rather than adding to a deprecated list.

Widening remotePatterns more than necessary

Setting hostname to ** or omitting pathname/search allows the optimizer to fetch and transform images from a much broader set of URLs than intended, which Next.js's own documentation flags as not recommended for exactly this reason — it can let the optimizer be pointed at URLs you didn't intend to serve. Scope patterns to the actual host and path your images come from.

Testing only the optimizer URL, never the original

If the original image URL itself is broken, no remotePatterns change will fix it. Always test the two independently, as described above.

Forgetting a full redeploy after a config change

A local fix that "still doesn't work in production" is frequently just a deployment that hasn't picked up the new next.config.js yet, not a wrong fix.

Verification checklist

  • The original image URL loads successfully in a browser tab, outside of next/image.
  • The decoded url parameter from the failing /_next/image request matches an entry in remotePatterns (protocol, hostname, port, and path).
  • next.config.js was rebuilt/redeployed after the configuration change, not just saved.
  • The /_next/image request now returns a successful status in the deployed environment.
  • No unnecessarily broad remotePatterns entry (bare ** hostname, unrestricted search) was left in place after debugging.

Claims to manually verify before publishing

  • Claim: A specific next/image 400 error you encountered matches the causes described here.

    • Why verification is needed: This article is written from Next.js documentation, not a captured incident with a specific image host or hosting provider.
    • Suggested evidence: The sanitized failing request URL (with any tokens removed), its response body, and the remotePatterns configuration before and after the fix.
  • Claim: remotePatterns behavior described here matches the Next.js version this project runs.

    • Why verification is needed: remotePatterns gained the search field and array-of-URL support in specific later versions; older versions behave differently.
    • Suggested evidence: next --version output and the matching version's images configuration documentation.
  • Claim: The production hosting platform in use caches builds or configuration in a way that delays a remotePatterns fix from taking effect.

    • Why verification is needed: This is general caution, not a confirmed behavior of a named hosting provider for this project.
    • Suggested evidence: The specific hosting provider's deployment/cache documentation and a sanitized before/after deploy timestamp showing when the fix actually took effect.

Related writing

Share