Skip to content
Docs

How to reduce Vercel Image Optimization costs

Learn how to reduce Vercel Image Optimization costs in Next.js by tuning cache TTLs, image sizes, formats, and quality. Diagnose high image transformation count and verify savings using the Vercel Dashboard.

Pat Beecher

Vercel Image Optimization transforms your source images into the right format, quality, and size for each device, then caches the result on Vercel’s content delivery network (CDN). Vercel bills you on three meters:

  • Image transformations: one per cache MISS and one per STALE revalidation.
  • Image cache writes: storing transformed bytes in the global cache, measured in 8 KB units.
  • Image cache reads: serving transformed bytes from the global cache, also measured in 8 KB units.

Three things drive those meters: transformation cardinality, transformed file size, and cache time to live (TTL). Each distinct cache key creates its own transformation and cache entry, and a large transformed output can consume several 8 KB read and write units. The number of possible variants depends on the combinations of source images, qualities, widths, and negotiated formats. For example, a single hero image requested in 2 formats and 2 qualities at 8 widths permits up to 32 cache entries. Vercel bills delivery of the optimized bytes separately, as Fast Data Transfer and CDN Requests.

This guide targets a production Next.js 16 App Router app on Vercel. Image Optimization works with other frameworks, but these configuration examples are Next.js-specific. You should aim for two results after deployment: a lower transformation count in Observability and cache reads growing relative to cache writes. A write-heavy ratio means you pay to create transformations that are never reused.

Copy link to headingHow transformations multiply

The Image Optimization API resolves every request through the cache, and what you pay depends on which of three states it returns:

Cache statusWhat happensWhat you're billed
HITServed from the regional or global cacheCache read, global cache only
MISSFetched, transformed, cached, and servedTransformation and cache write
STALEServed stale while revalidating in the backgroundTransformation and cache write

Three properties of that cache model drive cost:

  • Cardinality: formats, qualities, and widths multiply. Trimming any one dimension proportionally reduces the total.
  • Size: Vercel bills cache reads and writes in 8 KB units, so a smaller transformed file costs fewer units and less Fast Data Transfer. Lower quality and narrower widths shrink every output.
  • TTL: the cache lifetime of a transformation is whichever is larger, the upstream image's Cache-Control: s-maxage or max-age value, or your minimumCacheTTL setting. After the TTL expires, a cached variant is revalidated when it is next requested.

Vercel keys optimized images by project ID, q, w, url (or the content hash for local images), and the normalized Accept header. Vercel negotiates format using Accept, so each extra allowed format adds a cached variant for the browsers that request it.

Remote sources include Amazon S3, a content management system (CMS), and a digital asset manager (DAM). When the upstream cache lifetime is short or absent, minimumCacheTTL determines the minimum interval before a requested variant can require revalidation. The image never has to change for that to happen.

A STALE hit triggers one transformation and cache write per region, not one per request, because Vercel collapses concurrent revalidations. A short TTL on a stable image still re-bills in every region that serves it. A globally distributed audience, therefore, produces one transformation per serving region per cycle for an image that never changes.

Three limits decide what Vercel optimizes at all:

  • A transformed image can be at most 10 MB.
  • A source image can be at most 8,192 pixels on each side.
  • Vercel transforms only image/jpeg, image/png, image/webp, and image/avif sources.

Vercel serves every other format, including animated GIF files, unchanged.

Copy link to headingStep 1: Read your current usage

Open Observability > Image Optimization in your team dashboard, then filter to the project you are investigating. Note three things:

  1. The transformation count for the last 30 days. This is your baseline.
  2. The ratio of cache writes to cache reads. A healthy project typically reads more than it writes. Writes at or above reads may mean you create transformations that aren’t used, which points to cardinality or TTL issues.
  3. The formats and qualities in use. More than one quality value, or a format you did not intend to serve, is an immediate win.

Read those numbers against the billing units. Rates vary by region, so check Regional Pricing for the regions that serve your traffic.

Cache reads have one nuance. If a request recently accessed an optimized image in the same region, Vercel serves it from the in-region cache and incurs no global cache-read unit charge.

Vercel never bills a Hobby team on demand. Once a Hobby team passes the allowance, a new optimization returns HTTP 402 instead of an image. That fires the onError callback and renders the alt text. Images already in the cache continue to work. If only some images break, check whether they are new variants or uncached source URLs.

Copy link to headingStep 2: Fix the multipliers

These changes can reduce billed usage. Work through them in order because the first two act on every image in the project, so start there.

Copy link to headingFix 1: Raise the cache TTL for stable images

In Next.js 16, minimumCacheTTL defaults to 4 hours (14400 seconds), up from 60 seconds in Next.js 15. The effective TTL is the larger of minimumCacheTTL and the upstream Cache-Control age.

When the upstream cache lifetime is short or absent, minimumCacheTTL sets the minimum freshness period. A variant requires revalidation only when requested after its effective TTL expires.

If your images do not change within a month, set the TTL explicitly:

next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
minimumCacheTTL: 2678400, // 31 days
},
}
export default nextConfig

Where you control the upstream, such as your own Amazon S3 bucket, Vercel Blob store, or asset server, set a long Cache-Control: max-age=N there too. For images bundled with your app, use static imports: Next.js hashes the filename and caches the file for a year as immutable.

Use a longer TTL for stable images, and invalidate specific images on demand when they change. To update an image in place, use one of these:

  • Purge the cache from the dashboard.
  • Run vercel cache invalidate --srcimg /images/hero.png from the command-line interface (CLI).
  • Call invalidateBySrcImage() from @vercel/functions.
  • Change the src. A stable content hash or version in the filename is the most reliable option.

Choosing Invalidate in the dashboard, or invalidating a source image via the CLI or SDK, marks existing variants as stale for revalidation on their next request. Changing src creates a new cache key instead.

Copy link to headingFix 2: Serve one format

formats defaults to ['image/webp']. Adding AVIF alongside WebP doubles the transformations and cache entries for every image. Vercel caches an AVIF variant for browsers that support it and a WebP variant for the rest. Keep the default when cost is the priority:

next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
formats: ['image/webp'],
},
}
export default nextConfig

Trade-off: AVIF generally produces smaller files than WebP but takes longer to encode, which can increase latency on a cache miss. A transformation costs the same whatever the format, so the real trade is more cache entries against lower Fast Data Transfer from smaller AVIF bytes. Measure both against your own traffic before you decide.

Copy link to headingFix 3: Allow one quality

Every extra quality value multiplies the variants for the images that use it: two qualities produce twice the cache entries. Next.js 16 defaults qualities to [75] and requires the allowlist.

next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
qualities: [75],
},
}
export default nextConfig

The allowlist also caps what a per-component quality prop can produce. Next.js 16 coerces a quality prop outside the allowlist to the closest allowed value. For example, quality={80} uses the 75 variant instead of creating a new one. A direct request to /_next/image with a disallowed q returns HTTP 400.

On Next.js 15 and earlier, qualities is opt-in, and an unlisted value triggers an error rather than being coerced to an allowed value. Audit your quality props before you set the allowlist there. To learn more, see the Next.js 16 qualities breaking change and the qualities configuration reference.

Copy link to headingFix 4: Choose widths that match your layout

Next.js generates candidate image URLs in srcset. Browsers choose which candidates to request; listing a width does not automatically create a billed transformation.

Copy link to headingSet project-wide width limits

Use deviceSizes and imageSizes to control which widths Next.js can generate.

SettingNext.js 16 defaultsPurpose
deviceSizes[640, 750, 828, 1080, 1200, 1920, 2048, 3840]Provides device-width candidates
imageSizes[32, 48, 64, 96, 128, 256, 384]Adds smaller candidates for images using sizes

Choose a maximum width based on both layout size and supported pixel density:

Required image width = rendered CSS width × device pixel ratio

For example, an image displayed at 960 CSS pixels needs a 1,920px candidate for a 2x display. If that is your project’s largest requirement, you can remove 2,048 and 3,840 from deviceSizes.

Trimming imageSizes removes smaller candidates; it does not lower the default maximum width.

Trade-off: Removing widths can reduce the number of requested variants, but setting the maximum too low can make images look soft on high-density displays. Check your largest layouts and target devices before deploying.

These settings define the available widths across your project. Next, use each image’s dimensions and sizes prop to match those widths to its layout.

Copy link to headingFor fixed-size images, provide dimensions

Use explicit dimensions when an image’s rendered size is fixed:

<Image src="/hero.jpg" width={1200} height={600} alt="Hero" />

Without a sizes prop, this generates two candidates with the Next.js 16 defaults:

Pixel densityTarget widthGenerated width
1x1,200px1,200px
2x2,400px3,840px

Next.js selects the next allowed width that can satisfy the target. Because the defaults have no width between 2,048px and 3,840px, the 2x candidate jumps to 3,840px.

Copy link to headingFor responsive images, describe the layout with sizes

A fill image without sizes defaults to full-viewport sizing and generates eight device-width candidates, up to 3,840px:

<Image src="/hero.jpg" fill alt="Hero" />

If the image occupies only part of the viewport, tell the browser how much space it uses:

<Image
src="/thumb.jpg"
fill
sizes="(max-width: 768px) 50vw, 25vw"
alt="Thumbnail"
/>

This tells the browser to choose a candidate for an image that occupies:

  • Half the viewport at viewport widths up to 768px.
  • A quarter of the viewport above 768px.

Match these values to your actual layout. The fill image also needs a positioned parent with defined dimensions.

Next.js uses the smallest vw value to filter out smaller candidates, not to cap the largest width. Fixed-pixel values still help browsers choose an appropriate image, even though Next.js generates the full candidate list.

Copy link to headingFix 5: Skip optimization where it cannot help

Some assets may gain little from optimization: logos, icons, images under 10 KB, and animated GIF files. Set unoptimized on those:

<Image src="/logo.png" width={120} height={40} alt="Logo" unoptimized />

Tip: Next.js applies unoptimized automatically when src ends in .svg.

Trade-off: set this per image, not globally. Disabling optimization globally can increase transferred bytes and worsen Largest Contentful Paint (LCP), especially for large photos. The effect on Vercel Fast Data Transfer also depends on where the original images are served.

Copy link to headingFix 6: Allowlist what Vercel can optimize

remotePatterns and localPatterns restrict which URLs the optimization API accepts. In addition to the security benefit, an allowlist can reduce unnecessary optimization requests in two ways:

  • It prevents the optimizer from accepting source URLs that don't match your allowed patterns.
  • It turns a misbehaving source URL into an HTTP 400, rather than an unbounded stream of new source images. A CMS that appends a unique token per render is the common case.
next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
port: '',
pathname: '/your_account_id_here/**',
search: '',
},
],
},
}
export default nextConfig

If query parameters are required, allow the expected query string with search. Where your source supports stable URLs, remove unnecessary variable parameters before passing the URL to Image and preserve parameters required for authorization or selecting the correct image.

Copy link to headingStep 3: Investigate with an agent

This step is optional. If you use a coding agent with access to your repository and the Vercel CLI, the prompt below runs the investigation. Keep the agent read-only until you approve a change.

Agent Prompt

Investigate why Image Optimization usage is high on this Vercel project. Start read-only: do not edit files, change settings, purge caches, or deploy without my approval. 1. Read next.config.ts or next.config.js and report the effective values of images.formats, images.qualities, images.minimumCacheTTL, images.deviceSizes, images.imageSizes, images.remotePatterns, and images.localPatterns, noting which are defaults for our Next.js version. 2. Grep all Image component usages. Flag quality props with more than one distinct value, fill or responsive images missing a sizes prop, and small static assets such as logos and icons that could set unoptimized. 3. Identify remote image hosts and, where possible, the Cache-Control max-age they return, to estimate revalidation frequency. 4. Check whether any image src URLs include variable query strings or tokens that would create unbounded unique source images. 5. Return each finding, the config or component change that fixes it, the trade-off, and what I should watch in Observability, Image Optimization after deploy to verify it worked.

Give the agent your baseline numbers from Step 1 so it can prioritize. To let the agent pull usage data live, connect the Vercel Model Context Protocol (MCP) server.

Copy link to headingStep 4: Verify after you ship

Config changes take effect on the next deployment, and existing cache entries serve until their cache entries expire, so verification runs over a full cache cycle:

  1. Deploy the config changes.
  2. In Observability → Image Optimization, set the date filter to compare the weeks before and after the deploy. The transformation count declines as old entries age out. With a 31-day TTL, the full effect takes a cache cycle, not a day.
  3. Compare transformation and cache-write usage over periods with similar image-request volume.
  4. Spot-check visual quality on your heaviest pages at mobile and desktop widths, especially if you removed AVIF or trimmed widths.
  5. Check Fast Data Transfer over the same period. If it rose after you removed a format or added unoptimized, re-run the trade-off against your own traffic. Transformation, read, and write rates vary by region, so use the rates for the regions that serve your traffic.

Pro teams should also configure Spend Management to get an alert or to pause projects automatically at a spend threshold. This helps limit the impact of future usage spikes, such as a new image host or a CMS change to URL structure.

Copy link to headingFrequently asked questions

Why is my Vercel image transformation count high?

One source image can produce multiple variants for different widths, quality settings, and output formats. Each requested variant can incur a transformation on a cache miss or stale revalidation. Short cache lifetimes and changing source URLs, such as URLs with unique query parameters, can increase usage.

Does every srcset width create a billed transformation?

No. Generating a srcset only provides candidate image URLs for the browser. Vercel transforms a variant when it is requested and requires optimization because of a cache miss or stale revalidation. An accurate sizes prop helps browsers select an appropriate width.

Does redeploying clear the image cache?

No. Redeploying does not automatically clear Vercel’s image cache. To refresh an image, invalidate its cached variants or change its source URL. For local images, changing the file contents and redeploying changes the content hash used in the cache key.

Does increasing minimumCacheTTL reduce costs?

It can reduce transformation and cache-write charges for images requested repeatedly over time. A longer TTL allows cached variants to remain fresh longer. The effective cache lifetime is the greater of minimumCacheTTL and the upstream cache lifetime. Use longer TTLs for stable images, with invalidation or versioned URLs when content changes.

Should I disable image optimization?

Use the unoptimized prop selectively for images that gain little from optimization, such as small icons, SVGs, or animated GIFs. Disabling optimization globally can increase transferred bytes and slow image loading, especially for large photos. Compare total delivery cost and image performance before making the change.

Why do new images return HTTP 402 on Hobby?

When a Hobby team exceeds its Image Optimization usage limits, new image optimizations return HTTP 402. Previously optimized images can continue to load from cache, which explains why only some images may fail. Check your team’s usage, reduce unnecessary optimizations, or upgrade to Pro for additional usage.

Related documentation

More Vercel CDN guides