How to Fix: Setting cache-control header for icon routes results in duplicate header values
Duplicate Cache-Control headers on Next.js icon routes happen because two different header systems target the same response.
When you apply a custom cache-control rule in next.config.mjs for icon-related paths such as /icon or generated metadata asset routes, Next.js may already be attaching its own caching policy. The result is a response with duplicate header values instead of one authoritative directive, which can confuse browsers, CDNs, and debugging tools.
Understanding the Root Cause
In modern Next.js applications, special metadata files and routes such as icons can be handled by the framework itself. That includes generated assets like app router metadata endpoints, where Next.js may set default HTTP caching headers automatically.
The bug appears when you also define a matching rule inside the headers() function in next.config.mjs. Instead of replacing the existing header, the framework behavior for that route can cause the response to include multiple Cache-Control values.
Technically, this happens because:
- The icon route is a special framework-managed route.
- Next.js may already assign a caching policy for that asset response.
- Your custom
headers()rule adds anotherCache-Controlheader on top of the existing one. - For these routes, the final response may serialize both values rather than overwrite one cleanly.
This is why the issue is typically visible only on icon routes or other metadata-related endpoints, while normal application routes may behave as expected.
If you want to review or track framework behavior, refer to the reproduction sandbox.
Step-by-Step Solution
The safest fix is to avoid setting custom Cache-Control headers for framework-managed icon routes through next.config.mjs. Instead, use one of the following approaches depending on your use case.
1. Remove the conflicting header rule from next.config.mjs
If your current configuration targets icon routes directly, remove that rule first.
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/icon',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
]
},
}
export default nextConfig
Update it by deleting the icon-specific header entry:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
// Keep only non-conflicting custom headers here
]
},
}
export default nextConfig
2. Let Next.js manage caching for generated icon metadata routes
If the route is produced by the App Router metadata system, the framework should own the response headers. This avoids duplicate values and keeps behavior aligned with internal optimization logic.
Examples of routes you should generally not override with headers() include framework-generated icon endpoints and metadata asset paths.
3. If you need custom caching, serve the icon from a static public asset instead
If your goal is strict cache policy control, move the icon to the public directory and target that explicit file path rather than the special metadata route.
public/favicon.ico
Then apply the header to the concrete static file path:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/favicon.ico',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
]
},
}
export default nextConfig
This works better because /favicon.ico in public assets is a more predictable static file response than a framework-managed icon route.
4. Verify the response headers
After updating the config, test the route in browser devtools or with a network client.
curl -I http://localhost:3000/icon
curl -I http://localhost:3000/favicon.ico
You want to confirm that the final response contains a single Cache-Control header value.
5. Prefer route-specific ownership over global overrides
A good rule is simple: when Next.js generates the response, let Next.js manage its low-level headers unless the framework explicitly supports overriding them for that route type.
Common Edge Cases
Using both App Router metadata icons and public favicon files
If your project defines an icon through metadata APIs and also ships a public/favicon.ico, you can end up debugging the wrong route. Make sure you know which asset URL the browser is actually requesting.
Applying broad wildcard header rules
A pattern like /:path* may unintentionally match metadata or icon endpoints. Even if you did not target icons directly, a broad rule can still cause duplicated or conflicting caching behavior.
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=3600',
},
],
},
]
}
If you use wide patterns, audit whether they touch special routes.
CDN or reverse proxy adds another cache header
Sometimes the duplicate header is not only from Next.js. A reverse proxy, hosting platform, or CDN rule may append its own cache directive. Check your deployment stack if the problem appears only outside local development.
Different behavior between development and production
Header handling can vary depending on build mode, platform adapters, and optimization layers. Always validate in a production build before concluding the issue is resolved.
npm run build
npm run start
Trying to force overwrite semantics
Developers often assume a later header definition replaces an earlier one. On special routes, that assumption may not hold. If the framework appends during response assembly, your custom value may coexist rather than override.
FAQ
Why does this happen only on icon routes and not everywhere?
Because icon endpoints can be framework-managed metadata routes. Next.js may attach built-in caching for those responses, so adding another Cache-Control header through next.config.mjs creates duplication.
Can I still customize caching for icons?
Yes, but the most reliable option is to serve the icon as a normal static asset from the public directory and apply headers to that explicit file path instead of the framework-generated icon route.
Is this a browser problem or a Next.js problem?
The underlying issue is in how the response headers are composed for that route. Browsers simply receive the duplicated Cache-Control values. The practical fix is to avoid conflicting header ownership for icon responses.
The core takeaway is straightforward: do not attach custom Cache-Control headers to Next.js-managed icon routes through headers() when the framework already controls caching. Either let Next.js own the header or move the asset to a standard static file path where you fully control the response.