Vercel CDN Cache
Vercel's CDN caches your content (including pages, API responses, and static assets) in data centers around the world, closer to your users than your origin server. When someone requests cached content, Vercel serves it from the nearest region, cutting latency, reducing load on your origin, and making your site feel faster everywhere.
CDN caching is available for all deployments and domains on your account, regardless of the pricing plan.
There are two ways to cache content:
- Static file caching is automatic for all deployments, requiring no manual configuration
- To cache dynamic content that doesn't require real-time updates, use Incremental Static Regeneration. For more granular control, you can use
Cache-Controlheaders. Review How to cache responses to learn more.
To learn about cache keys, manually purging the cache, and the differences between invalidate and delete methods, see Purging Vercel CDN cache
CDN cache is best when you want to cache complete HTTP responses (entire pages, API responses, or static assets) at the edge, close to your users, such as in the following scenarios:
- Static pages that are the same for all users
- API responses that don't change frequently
- Static assets like images, fonts, and JavaScript bundles
- Server-rendered pages with predictable cache lifetimes
CDN Cache isn't the right fit when:
- You need user-specific content without the
Varyheader (consider Runtime Cache) - Responses include sensitive user data
- Content changes on every request to the same url
You can cache responses on Vercel with Cache-Control headers defined in:
- Responses from Vercel Functions
- Route definitions in
vercel.jsonornext.config.js
You can use any combination of the above options, but if you return Cache-Control headers in a Vercel Function, it will override the headers defined for the same route in vercel.json or next.config.js.
To cache the response of Functions on Vercel's CDN, you must include Cache-Control headers with any of the following directives:
s-maxage=Ns-maxage=N, stale-while-revalidate=Zs-maxage=N, stale-while-revalidate=Z, stale-if-error=Z
The following example demonstrates a function that caches its response and revalidates it every 1 second:
export async function GET() {
return new Response('Cache Control example', {
status: 200,
headers: {
'Cache-Control': 'public, s-maxage=1',
'CDN-Cache-Control': 'public, s-maxage=60',
'Vercel-CDN-Cache-Control': 'public, s-maxage=3600',
},
});
}For direct control over caching on Vercel and downstream CDNs, you can use CDN-Cache-Control headers.
You can define route headers in vercel.json or next.config.js files. These headers will be overridden by headers defined in Function responses.
The following example demonstrates a vercel.json file that adds Cache-Control headers to a route:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"headers": [
{
"source": "/about.js",
"headers": [
{
"key": "Cache-Control",
"value": "s-maxage=1, stale-while-revalidate=59"
}
]
}
]
}If you're building your app with Next.js, you should use next.config.js rather than vercel.json. The following example demonstrates a next.config.js file that adds Cache-Control headers to a route:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async headers() {
return [
{
source: '/about',
headers: [
{
key: 'Cache-Control',
value: 's-maxage=1, stale-while-revalidate=59',
},
],
},
];
},
};
module.exports = nextConfig;See the Next docs to learn more about next.config.js.
Static files are automatically cached on Vercel's global network for the lifetime of the deployment after the first request.
- If a static file is unchanged, the cached value can persist across deployments due to the hash used in the filename
- Optimized images cached will persist across deployments for both static images and remote images
max-age=N, publicmax-age=N, immutable
Where N is the number of seconds the response should be cached. The response must also meet the caching criteria.
Immutable static assets are files under /_next/static/immutable/ and
/_vercel/immutable/. They are content-addressed, and Vercel serves them with
Cache-Control: public, max-age=31536000, immutable. If you accidentally ship
a secret, credential, or other must-remove content inside an immutable bundle,
you can delete the stored asset immediately instead of waiting for the cache to
expire.
Send a POST request with the asset's path, without a leading slash:
curl -X POST \
'https://api.vercel.com/v1/edge-cache/dangerously-delete-immutable-static?projectIdOrName=my-project&teamId=my-team' \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"path": "_next/static/immutable/chunks/example-chunk.js"}'A successful request does three things:
- Permanently deletes the stored asset for both the production and preview environments of the project
- Blocks the asset's URL with a
410 Goneresponse for seven days across Vercel's CDN, effective immediately. This includes copies already cached at the edge. After the block expires, the URL returns404 - Rotates the project's build hash salt, so your next deployment emits different asset URLs
Keep these behaviors in mind:
- Deleting the asset does not delete the content. The immutable store addresses assets by their content. If the sensitive content still exists in your source, your next deployment uploads the same bytes again. Remove or replace the content in your source code, deploy, and then delete the old asset
- To recover from a mistaken deletion, deploy again. The build hash salt
rotation means the new build references new asset URLs, so your site works
immediately. The deleted URL keeps returning
410or404, but nothing references it anymore - The
pathmust exactly match the stored asset path and start with_vercel/immutable/or_next/static/immutable/. If no asset exists at the path, the API returns404 asset_not_foundand blocks nothing. Projects using abasePathor a microfrontends service prefix store assets under that prefix, and you cannot delete those assets with this endpoint - You need project-update permission on the team, and Vercel rate limits requests
- If you serve Vercel through your own CDN, purge it there too. The
410block covers Vercel's CDN, but your CDN may hold the asset with its original one-year immutable cache header until you purge it
You can cache dynamic content through Vercel Functions, including SSR, by adding Cache-Control headers to your response. When you specify Cache-Control headers in a function, responses will be cached in the region the function was requested from.
See our docs on Cache-Control headers to learn how to best use Cache-Control directives on Vercel's CDN.
Vercel supports two Targeted Cache-Control headers:
CDN-Cache-Control, which allows you to control the Vercel CDN Cache or other CDN cache separately from the browser's cache. The browser will not be affected by this headerVercel-CDN-Cache-Control, which allows you to specifically control Vercel's Cache. Neither other CDNs nor the browser will be affected by this header
By default, the headers returned to the browser are as follows:
Cache-ControlCDN-Cache-Control
Vercel-CDN-Cache-Control headers are not returned to the browser or forwarded to other CDNs.
To learn how these headers work in detail, see our dedicated headers docs.
The following example demonstrates Cache-Control headers that instruct:
- Vercel's Cache to have a TTL of
3600seconds - Downstream CDNs to have a TTL of
60seconds - Clients to have a TTL of
10seconds
export async function GET() {
return new Response('Cache Control example', {
status: 200,
headers: {
'Cache-Control': 'max-age=10',
'CDN-Cache-Control': 'max-age=60',
'Vercel-CDN-Cache-Control': 'max-age=3600',
},
});
}If you set Cache-Control without a CDN-Cache-Control, the Vercel CDN strips s-maxage and stale-while-revalidate from the response before sending it to the browser. To determine if the response was served from the cache, check the x-vercel-cache header in the response.
The Vary response header instructs caches to use specific request headers as part of the cache key. This allows you to serve different cached responses to different users based on their request headers.
When Vercel's CDN receives a request, it combines the cache key (described in the Cache Invalidation section) with the values of any request headers specified in the Vary header to create a unique cache entry for each distinct combination.
Some request headers carry a value that's close to unique per visitor. Cookie is the clearest example, since session and analytics cookies differ for everyone. Varying on a header like this gives nearly every request its own cache entry, so almost nothing is served from the cache and the entries that are written are unlikely to be read again.
Vercel's CDN doesn't cache a response whose Vary names one of these headers:
| Header | Why it isn't cacheable |
|---|---|
Cookie | Session and analytics cookies give most visitors a distinct value. |
The response is still generated and served normally. It's returned with x-vercel-cache: MISS, the reason Vary key denied is recorded in runtime logs, and no cache entry is written for it.
If a route you expect to be cached returns x-vercel-cache: MISS with this reason, check the Vary header your origin sends. If the response doesn't actually change with that header, remove it from Vary and the response becomes cacheable again.
The most common use case for the Vary header is content negotiation, serving different content based on:
- User location (e.g.,
X-Vercel-IP-Country) - Language preferences (e.g.,
Accept-Language) - Response format for an API that serves more than one (e.g., a custom
X-Api-Version)
Pick the narrowest header that captures the difference. Varying on User-Agent to serve two layouts, for example, splits the cache across every browser and version string your visitors send, when a header that holds only the value you branch on would produce two entries.
Example: Country-specific content
You can use the Vary header with Vercel's X-Vercel-IP-Country request header to cache different responses for users from different countries:
import { type NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const country = request.headers.get('x-vercel-ip-country') || 'unknown';
// Serve different content based on country
let content;
if (country === 'US') {
content = { message: 'Hello from the United States!' };
} else if (country === 'GB') {
content = { message: 'Hello from the United Kingdom!' };
} else {
content = { message: `Hello from ${country}!` };
}
return Response.json(content, {
status: 200,
headers: {
'Cache-Control': 's-maxage=3600',
Vary: 'X-Vercel-IP-Country',
},
});
}You can set the Vary header in the same ways you set other response headers:
In Vercel Functions
import { type NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
return Response.json(
{ data: 'This response varies by country' },
{
status: 200,
headers: {
Vary: 'X-Vercel-IP-Country',
'Cache-Control': 's-maxage=3600',
},
},
);
}Using vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"headers": [
{
"source": "/api/data",
"headers": [
{
"key": "Vary",
"value": "X-Vercel-IP-Country"
},
{
"key": "Cache-Control",
"value": "s-maxage=3600"
}
]
}
]
}Using next.config.js
If you're building your app with Next.js, use next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/api/data',
headers: [
{
key: 'Vary',
value: 'X-Vercel-IP-Country',
},
{
key: 'Cache-Control',
value: 's-maxage=3600',
},
],
},
];
},
};
module.exports = nextConfig;You can specify multiple headers in a single Vary value by separating them with commas:
res.setHeader('Vary', 'X-Vercel-IP-Country, Accept-Language');This will create separate cache entries for each unique combination of country and language preference.
- Use
Varyheaders selectively, as each additional header exponentially increases the number of cache entries. This doesn't directly impact your bill, but can result in more cache misses than desired - Only include headers that meaningfully impact content generation
- Prefer a header with a small, known set of values. A header that's close to unique per visitor isn't cacheable at all, as described in High-cardinality headers
- Avoid varying on
Refererfor traffic-source rendering: browsers send only the referring origin cross-site, and same-origin navigation sends full URLs that fragment the cache. A query parameter, or reading the header in your function, captures the source without the fragmentation - Consider combining multiple variations into a single header value when possible
- Set
Varyon the routes that need it rather than globally in middleware or a proxy, so one header doesn't make every route uncacheable
The Cache-Control field is an HTTP header specifying caching rules for client (browser) requests and server responses. A cache must obey the requirements defined in the Cache-Control header.
For server responses to be successfully cached with Vercel's CDN, the following criteria must be met:
- Request uses
GETorHEADmethod. - Request doesn't contain
Rangeheader. - Request doesn't contain
Authorizationheader. - Response uses
200,404,410,301,302,307or308status code. - Response doesn't exceed
10MBin content length (20MBfor streaming Vercel Function responses, see Limits). - Response doesn't contain the
set-cookieheader. - Response doesn't contain the
private,no-cacheorno-storedirectives in theCache-Controlheader. - Response doesn't contain
Vary: *header, which is treated as equivalent toCache-Control: private. - Response doesn't contain a
Varyheader naming a high-cardinality header, such asCookie.
Vercel doesn't allow bypassing the cache for static files by design.
To learn about cache keys, manually purging the cache, and the differences between invalidate and delete methods, see Purging Vercel CDN Cache.
The x-vercel-cache header is included in HTTP responses to the client, and describes the state of the cache.
See our headers docs to learn more.
Vercel's CDN Cache is segmented by region. The following caching limits apply to Vercel Function responses:
- Max cacheable response size:
- Streaming functions: 20MB
- Non-streaming functions: 10MB
- Max cache time: 1 year
s-maxagemax-agestale-while-revalidate
While you can put the maximum time for server-side caching, cache times are best-effort and not guaranteed. If an asset is requested often, it is more likely to live the entire duration. If your asset is rarely requested (e.g. once a day), it may be evicted from the regional cache.
Vercel doesn't currently support using proxy-revalidate for server-side caching. Vercel's CDN supports stale-if-error.
Was this helpful?