next.config.ts — Wrong images.remotePatterns Blocked All External Images in Production
A wildcard typo in images.remotePatterns blocked all external images in production for 6 hours.
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
- ✓Node.js 18+
- ✓Next.js 14+ project experience
- ✓Basic familiarity with JavaScript/TypeScript config files
images.remotePatternsrequires BOTHprotocolandhostname— missing either blocks all external images silently (no build error, images just 404 in production)- next.config.ts supports 50+ config options grouped into: Images, Routing (redirects/rewrites/headers), Build (webpack/turbopack), Experimental, Server (serverActions/output), and React (reactCompiler/strictMode)
output: 'export'disables ALL Next.js server features — API routes, ISR, middleware, draft mode. Useoutput: 'standalone'for self-hosted server deploymentsserverActionsallows configuring allowed origins — required when calling Server Actions from different domains (e.g., staging subdomain)reactCompiler: true(experimental) automatically memoizes components — eliminates most useMemo/useCallback calls at the cost of ~5% longer build time
Like a building security system that requires both a badge AND a face scan for entry — but the badge reader is broken and the security guard just shrugs. That's images.remotePatterns with a bad configuration: no error, no warning, every external image silently denied. The worst bugs are the ones that produce no errors, just empty image placeholders across your entire site.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
next.config.ts (or next.config.js for the JavaScript SDK) is the single configuration file that controls every aspect of Next.js behaviour — from how images are optimised to how routes are rewritten to how the React compiler optimises your components. With 50+ configurable options spanning 10 categories, it is simultaneously the most powerful and most misunderstood file in a Next.js project.
The most common production issue: images.remotePatterns configured incorrectly. A single missing field, wrong wildcard placement, or omitted protocol causes every external image in your application to return a 404 or fall back to the unoptimized original URL — silently, with no build errors or runtime warnings. This issue is the #1 support complaint across Next.js projects.
This article covers every meaningful option in next.config.ts: images (the most error-prone), routing (redirects, rewrites, headers, middleware config), build tooling (webpack, turbopack, bundle analysis), server configuration (serverActions, output, experimental features), React configuration (reactCompiler, strictMode), and environment variable handling. For each option, you get the production-ready configuration and the mistakes to avoid.
By the end, you will be able to read any next.config.ts and understand exactly what every line does — and more importantly, which lines are likely to cause production incidents.
images.remotePatterns — The #1 Configuration Footgun
The images config block controls how Next.js optimises images — resizing, format conversion (WebP/AVIF), quality, and caching. It is the most frequently misconfigured option in production. The remotePatterns array defines which external image hosts are allowed for optimisation.
Each remotePattern entry requires three fields: protocol ('http' or 'https'), hostname (exact domain or wildcard pattern), and optionally port and pathname. All fields are validated at request time when the /_next/image endpoint processes an image. If any field does not match, the image request returns 404.
The wildcard syntax: matches this and all subdomains. .example.com matches example.com, cdn.example.com, a.b.example.com. *.example.com (single asterisk) matches only one level — cdn.example.com but NOT a.b.example.com. Single asterisk is often used accidentally and causes intermittent failures.
The production pattern: explicitly list every domain that serves images. Prefer exact hostnames over wildcards where possible. Always specify both protocol and hostname. Add a CI check that validates every remotePattern entry against a whitelist of expected domains — if a domain is missing from the config but appears in the content, the CI should warn.
/_next/image with NO build warning, NO runtime console error, and NO visible error in the UI — just an empty grey box. The only way to catch it is to check the browser Network tab or write a Playwright test that asserts naturalWidth > 0 on images.hostname: '.amazonaws.com' (single asterisk) but their images were served from s3.us-east-1.amazonaws.com (two subdomain levels). The single asterisk matched only one level. Images from bucket.s3.amazonaws.com (two levels) worked but bucket.s3.us-east-1.amazonaws.com (four levels) did not. The fix: hostname: '*.amazonaws.com'. Rule: always test remotePatterns with a curl command against the production /_next/image URL for every image domain you use.protocol AND hostname — missing either blocks all external image optimisation silently.* matches any number of subdomain levels; matches only one — use ** unless you specifically need one-level matching./_next/image?url=https://your-domain.com/img.jpg&w=640&q=75 must return 200.Redirects, Rewrites, and Headers — Async Functions, Not Objects
A common mistake: defining redirects, rewrites, or headers as static arrays instead of async functions. Next.js expects these to be async functions that return an array — because in some configurations, you need to fetch external data (like feature flags) to determine the routing config.
The syntax: async . Each rule has redirects() { return [ / array of rules / ] }source (incoming URL pattern), destination (target URL), and permanent (true = 301, false = 307) for redirects. headers similarly returns an array of { source, headers: [{ key, value }] } objects.
Path patterns: use :param for named parameters, for zero-or-more wildcard, :path for catch-all segments. The pattern /blog/:slug matches /blog/hello-world with slug = 'hello-world'. The pattern /api/:path* matches /api/v1/users with path = 'v1/users'.
Performance implication: redirects and headers are evaluated on every request. Having 100+ redirect rules can add 10-50ms per request on cold start. For large redirect sets, consider a middleware-based approach or a CDN-level redirect (Vercel Edge Config, Cloudflare Bulk Redirects) instead.
:param, , :path). Destination paths use standard URL syntax with :param interpolation. The paths must share the same parameter names — source :slug must have destination :slug. Mismatched parameter names cause runtime errors.permanent: true (301) on an old URL structure. Three months later, they needed to revert the redirect but discovered that browsers had cached the 301 permanently — users on cached browsers could not access the new URL for months. Rule: always use permanent: false (307) for new redirects during a transition period. Switch to permanent: true only after you are certain the redirect is final. 301s are cached by browsers indefinitely.:param, , and :path syntax — parameters must match between source and destination.webpack and transpilePackages — When Dependencies Break the Build
Next.js abstracts Webpack configuration behind its own build pipeline. Most of the time, you don't touch Webpack directly. When you do — to add custom loaders, modify resolve aliases, or adjust SplitChunks — you must use the webpack config function.
The webpack function receives the default config and the build context ({ isServer, webpack, nextRuntime }). You modify the config and return it. Common customisations: adding SVG loaders (SVGR), configuring module aliases, adding bundle analysis plugins, and modifying resolve fallbacks for Node.js modules in browser context.
transpilePackages is a simpler alternative to custom webpack config for one specific use case: transpiling external npm packages that are not ESM-compatible. Some packages distribute only as CommonJS or untranspiled TypeScript. Adding them to transpilePackages tells Next.js to run them through the Babel/SWC pipeline, making them compatible with the browser build.
The production insight: every custom webpack configuration increases the risk of build failures when Next.js updates. The Webpack API is considered internal — Vercel changes it between major versions without deprecation warnings. Minimise custom webpack config and prefer Next.js-native solutions (transpilePackages, next/dynamic, next/image) whenever possible.
output — Static Export vs Server vs Standalone
The output option determines how Next.js builds your application. The default (no output specified) builds for a Node.js server with all Next.js features — API routes, ISR, middleware, draft mode, server actions. The output includes .next/ with server-side code that next start runs.
output: 'export' — static HTML export. Builds every page as static HTML/CSS/JS files. Output goes to out/ directory. Tradeoffs: zero server runtime (fastest, cheapest to host), NO API routes, NO ISR (all pages built at deploy time), NO middleware, NO draft mode, NO server actions. Images must be unoptimized. Suitable for: marketing sites, documentation, blogs where every page is pre-built.
output: 'standalone' — builds a self-contained deployment with minimal dependencies. Output includes a standalone/ directory with the server code and a minimal node_modules. Tradeoffs: larger output (includes server runtime), enables all Next.js features. Requires a Node.js runtime. Suitable for: Docker deployments, self-hosted production, any app that needs ISR or API routes.
The mistake: teams choose output: 'export' for performance, then discover they need ISR or API routes and must migrate — a significant effort. Choose output based on your feature requirements, not perceived performance.
output: 'export' then needed a search API endpoint. They had to deploy a separate serverless function (Cloudflare Worker) for the search API, creating two separate deployment pipelines and a CORS configuration challenge. Two months later they migrated to output: 'standalone' and consolidated everything into a single Next.js deployment. Rule: if you think you might need ANY server-side feature in the future, do not start with static export. The migration cost is high.serverActions — The Origin Check That Blocks Cross-Domain Requests
Server Actions in Next.js 16 include a security feature: they check the Origin header of incoming requests against an allowed list. If your app is accessed from multiple domains — for example, www.example.com and staging.example.com, or if you use a reverse proxy that changes the origin — Server Actions will return 403 Forbidden for requests from unlisted origins.
The configuration is serverActions.allowedOrigins: string[]. Each entry should be the origin (protocol + hostname, no path). The check applies to all Server Action calls, including those from client components and forms.
The production issue: teams deploy to a staging subdomain, test Server Actions, and get 403 errors. The staging domain is not in the allowed origins list. The fix is adding both the production and staging domains to the config. For local development, http://localhost:3000 must also be in the list.
Security consideration: the origin check prevents CSRF-style attacks where a malicious site tricks a user's browser into calling your Server Action. Only add trusted origins to the allowed list. Use environment variables to differentiate origins per environment.
process.env.VERCEL_URL is automatically set on Vercel deployments. For self-hosted, pass ALLOWED_ORIGINS as a comma-separated env var and parse it in next.config.ts.test.example.com that called the production Server Action. Every A/B test interaction returned 403 because test.example.com was not in allowedOrigins. The fix: added **.example.com as a wildcard pattern (not supported — wildcards don't work in allowedOrigins). The actual fix: added every known subdomain explicitly. Rule: if you have more than 3 domains, use a CDN with origin consolidation or switch to API routes with manual origin validation instead of Server Actions.experimental — Where Future Features Live (and Break)
The experimental config block contains features that are not yet stable. Using experimental features gives you access to cutting-edge capabilities but comes with risks: the API may change without notice, the feature may be removed, or it may have bugs that affect production.
Key experimental features in Next.js 16: mdxRs (Rust-based MDX compiler — faster builds but different output), turbo (enable Turbopack in development — default since 15.3), serverActions (now stable in 16, moved from experimental), reactCompiler (automatic memoization with React Forget), ppr (Partial Prerendering — experimental), optimizePackageImports (automatic tree shaking of barrel files from specific packages).
The discipline: pin the exact Next.js version when using experimental features. Never use latest in your package.json. Add a comment explaining why each experimental flag is enabled, what it does, and what the migration path is when it becomes stable. Audit experimental flags every quarter and remove those that have graduated to stable.
experimental.ppr: true during migration from Next.js 13 to 14. Six months later, they noticed stale data in dynamic sections of otherwise-static pages. The PPR implementation had changed between 14.0 and 14.2 — the revalidation behaviour was slightly different. The fix: disabling PPR, which required removing the experimental flag and adjusting page-level caching. Rule: experimental features change between minor versions. Pin both the Next.js version AND the experimental feature version in your docs.reactCompiler — Automatic Memoization Without useMemo
The React Compiler (formerly React Forget) is a build-time tool that automatically adds useMemo and useCallback calls to your components. It analyses component renders and determines which values need memoization to prevent unnecessary re-renders. Enabled via experimental.reactCompiler: true.
The impact: for a typical app with 50+ components, the compiler can eliminate 80-90% of manual memoization code. Components that re-render unnecessarily due to missing useMemo or inline function references are automatically optimised. The developer no longer needs to think about React.memo, useMemo, or useCallback — the compiler handles it.
The tradeoff: build time increases by approximately 5% (more for large component trees). The compiler's output is conservative — it may add memoization that is unnecessary, slightly increasing the generated bundle. Runtime behaviour is identical — the compiler only adds memoization that preserves existing semantics.
The production consideration: enable the compiler on a route-by-route basis if needed via the compilationMode option. For existing apps, enable it in a staging environment first and use React DevTools Profiler to verify that unnecessary re-renders are eliminated without breaking component behaviour.
compilationMode: 'strict' to surface warnings about patterns the compiler cannot safely optimise.useMemo and useCallback across 30 components. Enabling the React Compiler reduced the codebase by 450 lines of manual memoization code. Performance was identical — the compiler's output was as good as the manual memoization. Build time increased from 120s to 127s (6%). The team's next sprint removed all manual memoization calls and relied entirely on the compiler. Rule: the React Compiler eliminates 80-90% of manual memoization code with zero runtime cost. Enable it on new projects from day one.Turbopack Configuration — Matching the Rust Bundler to Your Needs
Turbopack is the default development bundler in Next.js 16. It replaces Webpack for next dev with a Rust-based implementation that is approximately 10x faster for cold starts and substantially faster for Hot Module Replacement (HMR). The configuration is minimal because Turbopack aims to work out of the box.
The turbo config block in experimental lets you customise Turbopack-specific settings: resolve aliases, load rules for specific file types, and webpack loader compatibility. Most projects don't need this — the default Turbopack config handles TypeScript, JSX, CSS, CSS Modules, and static assets.
Key limitation: Turbopack does not support custom webpack loaders. If you use SVGR for SVG imports, custom CSS post-processors, or GraphQL codegen imports, those features won't work with Turbopack. You must either: (1) disable Turbopack and use Webpack (--no-turbo flag), (2) convert the loader to a Turbopack-compatible transform, or (3) pre-process those files outside the bundler.
The production decision: use Turbopack for development (default, faster iteration). Use Webpack for production builds (more mature optimisation, custom loader support). Do not enable Turbopack for production builds unless you have verified identical output.
next dev (default in 16). Let Turbopack handle production only if you disable all custom webpack loaders and verify bundle output is equivalent.Environment Variables — Public vs Private and the NEXT_PUBLIC_ Prefix
Next.js handles environment variables through .env files (.env.local, .env.production, .env.development) and the env config option in next.config.ts. The critical distinction: which variables are available on the server vs the client.
Variables without the NEXT_PUBLIC_ prefix are server-only. They are available in getServerSideProps, API routes, Server Components, middleware, and next.config.ts itself — but NOT in client-side JavaScript. If you reference process.env.API_SECRET in a client component, it will be undefined at runtime (and replaced with the literal string at build time, potentially leaking the value if used in a string).
Variables prefixed with NEXT_PUBLIC_ are inlined at build time and available in client-side code. They are replaced with their actual values during the build — meaning any change to a NEXT_PUBLIC_ variable requires a rebuild to take effect. Use NEXT_PUBLIC_ for values that are safe to expose to the browser: API base URLs, feature flag names, public keys.
The env option in next.config.ts adds variables that are available in both server and browser contexts — essentially treating them as if they had the NEXT_PUBLIC_ prefix. Use this sparingly — it's better to be explicit with prefixes.
env option in next.config.ts inlines values into the client bundle at build time. Any value in env (or prefixed with NEXT_PUBLIC_) is visible in the browser's rendered HTML and JavaScript source. Never put API keys, database URLs, or auth tokens in the env config option or as NEXT_PUBLIC_ variables..env.production and imported it in a client component via NEXT_PUBLIC_DATABASE_URL. The connection string was inlined into the client-side JavaScript bundle, accessible to every user via View Source. The database was internet-exposed and was compromised within 2 hours. Rule: if it's a secret, keep the NEXT_PUBLIC_ prefix off. If it's safe for the browser, use NEXT_PUBLIC_ explicitly. The prefix system is a security boundary — respect it.env config option makes variables available in both server and client — use sparingly and never for secrets.Full Production Config — Putting It All Together
The production-grade next.config.ts combines all the patterns covered above into a single, well-documented configuration file. Every option is explained with a comment. Experimental features are gated by environment variables. Image remotePatterns are validated. Security headers are set. Environment-specific overrides are handled.
The key principle: the configuration should be self-documenting. Anyone on the team should be able to read the file and understand what every option does and why it's set. Use TypeScript for type safety — import type { NextConfig } from 'next' gives you autocomplete and validation.
Version control the configuration file in every environment. The file should be identical across development, staging, and production — differences should be controlled by environment variables, not by having different config files. Use .env.local for local overrides.
import type { NextConfig } from 'next' for TypeScript autocomplete and validation. This catches typos ('headders' instead of 'headers') at the type-checking stage, not in production.6 Hours of Broken Images — A Single Typo in remotePatterns
/_next/image?url=https://cdn.example.com/product.jpg returning 404. Next.js image optimizations silently failed. The site looked broken — empty grey boxes where product images should be. Internal monitoring did not catch it because image 404s were not tracked as errors.hostname: '*.example.com' and expected it to match cdn.example.com, images.example.com, and static.example.com. They also assumed that omitting the protocol field would default to both HTTP and HTTPS.images.remotePatterns. First, the protocol field was omitted — the team assumed it defaulted to HTTPS. It does not. When protocol is missing, Next.js does not match any protocol, so no remote images are optimised. Second, the wildcard syntax .example.com is incorrect — remotePatterns uses as the wildcard prefix, not . The correct pattern is .example.com.
The combination: missing protocol + wrong wildcard syntax = zero remote images passed the validation. Every image request to /_next/image failed a pattern check and returned 404. The images appeared in development because Next.js uses unoptimized images in dev mode — the team tested locally, saw images, and assumed the config was correct. Production uses optimized images via /_next/image, which enforces remotePatterns.
The fix: corrected remotePatterns to include protocol: 'https' and hostname: '.example.com'. After deploying the fix, all images rendered within 30 seconds (ISR regenerated the optimised images). Added a CI check that validates remotePatterns against a list of expected image domains. Added visual regression testing that catches broken images in staging before deploy.remotePatterns configuration: added protocol: 'https' and changed hostname from '.example.com' to '*.example.com'. Added a validation script in CI that checks the remotePatterns config against a whitelist of expected domains and fails if any domain is missing. Added a Playwright test that loads each page type and asserts that product images have non-zero naturalWidth (not broken). The 6-hour gap between deploy and detection prompted adding synthetic monitoring that checks image load status on the homepage every minute.- images.remotePatterns requires BOTH protocol AND hostname — missing either silently blocks all remote image optimisation with no build error
- The wildcard prefix for remotePatterns hostname is
*(double asterisk), NOT(single asterisk) —**.example.commatches all subdomains - Development mode does NOT optimise images — what works in dev may fail in production where
/_next/imageenforces remotePatterns validation - Add image monitoring: visual regression tests that catch broken images, and synthetic checks that verify image URLs return 200 in production
/_next/image which validates against remotePatterns. Verify protocol AND hostname are correctly specified with proper wildcard syntaxcurl -I to check response status codescat next.config.ts | grep -A20 'images' || cat next.config.js | grep -A20 'images'curl -sI 'http://localhost:3000/_next/image?url=https://external.com/img.jpg&w=640&q=75' | head -10Key takeaways
* not , and has no build-time validationInterview Questions on This Topic
You deploy a Next.js 16 app to production and all external images return 404 via next/image. The images work fine in local development. Walk through your debugging process.
/_next/image?url=... — these should return 200 with an optimized image but return 404. This confirms the image optimization pipeline is rejecting the request.
Next, I'd check the next.config.ts file's images.remotePatterns configuration. The most likely issue: the remotePatterns array is either empty, missing the specific domain, or has incorrect fields. I'd run: cat next.config.ts | grep -A20 'images' to inspect.
Common root causes: (1) protocol field missing — remotePatterns requires BOTH protocol and hostname. If protocol is omitted, no protocol matches. (2) Wrong wildcard syntax — .example.com (single asterisk) matches only one subdomain level. *.example.com (double asterisk) is needed for multiple levels. (3) The domain is missing from remotePatterns entirely — a common issue when adding a new image CDN.
To verify the fix, I'd test with curl: curl -sI 'https://example.com/_next/image?url=https://cdn.example.com/img.jpg&w=640&q=75'. A 200 response confirms the fix. I'd also add a Playwright test that asserts images on key pages have naturalWidth > 0 to catch this in CI.
Prevention: add a validation script that checks that every domain in your content's image URLs is covered by a remotePatterns entry. Fail CI if a domain is missing.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
That's Next.js. Mark it forged?
7 min read · try the examples if you haven't