---
title: How to reduce Vercel Image Optimization costs
description: 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.
url: "https://vercel.com/kb/guide/reduce-image-optimization-costs-on-vercel"
published: 2026-09-17
last_updated: 2026-09-17
authors: Pat Beecher
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

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 produces 32 cache entries. Vercel bills delivery of the optimized bytes separately, as [Fast Data Transfer and Edge Requests](https://vercel.com/docs/manage-cdn-usage).

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](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fobservability%2Fimage-optimization&title=Go%20to%20Observability) and cache reads growing relative to cache writes. A write-heavy ratio means you pay to create transformations that are never reused.

## How 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 status | What happens                                      | What you're billed             |
| ------------ | ------------------------------------------------- | ------------------------------ |
| HIT          | Served from the regional or global cache          | Cache read, global cache only  |
| MISS         | Fetched, transformed, cached, and served          | Transformation and cache write |
| STALE        | Served stale while revalidating in the background | Transformation 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.

## Step 1: Read your current usage

Open [Observability > Image Optimization](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fobservability%2Fimage-optimization&title=Go%20to%20Observability) 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 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](https://vercel.com/docs/pricing/regional-pricing#specific-region-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](https://vercel.com/docs/image-optimization/limits-and-pricing#hobby) 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.

## Step 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.

### Fix 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:

```tsx
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](https://nextjs.org/docs/app/api-reference/components/image#responsive-images-with-a-static-export): 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](https://vercel.com/docs/caching/cdn-cache/purge).
  
- 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.

### Fix 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:

```tsx
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.

### Fix 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.

```tsx
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](https://nextjs.org/docs/app/guides/upgrading/version-16#qualities-default-breaking-change) and the [qualities configuration reference](https://nextjs.org/docs/app/api-reference/components/image#qualities).

### Fix 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.

#### Set project-wide width limits

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

| Setting       | Next.js 16 defaults                             | Purpose                                          |
| ------------- | ----------------------------------------------- | ------------------------------------------------ |
| `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:

```plaintext
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.

#### For fixed-size images, provide dimensions

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

```tsx
<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 density | Target width | Generated width |
| ------------- | ------------ | --------------- |
| 1x            | 1,200px      | 1,200px         |
| 2x            | 2,400px      | 3,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.

#### For 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:

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

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

```tsx
<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.

### Fix 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:

```tsx
<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.

### Fix 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.
  

```tsx
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.

## Step 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

```txt
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](https://vercel.com/docs/agent-resources/vercel-mcp).

## Step 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](https://vercel.com/d?to=%2F%5Bteam%5D%2F~%2Fobservability%2Fimage-optimization&title=Go%20to%20Observability), 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](https://vercel.com/docs/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.

## Related resources

- Explore [Vercel’s image optimization cost guidance](https://vercel.com/docs/image-optimization/managing-image-optimization-costs) for reducing billed usage.
  
- Understand [Vercel’s image optimization pricing](https://vercel.com/docs/image-optimization/limits-and-pricing), limits, and usage billing meters.
  
- Configure [Next.js images](https://nextjs.org/docs/app/api-reference/components/image) using documented sizing, quality, format, and caching.
  
- Learn how to invalidate cached images through [Vercel’s purging tools](https://vercel.com/docs/caching/cdn-cache/purge).
  
- Explore [advanced Next.js image optimization](https://vercel.com/academy/nextjs-foundations/advanced-image-optimization) techniques for responsive production applications.