Skip to content
Cedric Kato, home

FN-003 — Field note ·

Why my images went blank: Vercel's 402 image-optimization error

A 402 on /_next/image isn't a code bug. It's Vercel's shared Image Optimization quota. How I traced blank images on a Next.js site, and three ways to fix it.

A 402 from /_next/image on Vercel means your team's Image Optimization allowance is used up. At the time of writing, Vercel's limits page gives Hobby teams 5,000 image transformations a month; past that, new image sizes fail while cached ones keep loading. The fix is to turn optimization off, pre-generate the sizes or upgrade the plan.

I ran into it in September 2026, when the project screenshots on my homepage went blank.

The symptom: empty image frames

  • Each project card kept its frame and caption, but the screenshot inside was missing.
  • The browser console showed errors for the /_next/image?url=... requests.

Nothing in those symptoms points at billing. From the page alone, it looks like a broken component or a bad image path.

Why /_next/image returns 402 on Vercel

With next/image, the browser never asks for your file directly. The component rewrites it into /_next/image URLs, one per width, and the server resizes and re-encodes the image on request. Here is the logo on the sign-in page of Motherboard, one of my demos, as the browser's Elements panel shows it (trimmed):

<img alt="Example Company" width="140" height="140"
  srcset="/_next/image?url=%2Flogo-white.png&w=256&q=75 1x,
          /_next/image?url=%2Flogo-white.png&w=384&q=75 2x"
  src="/_next/image?url=%2Flogo-white.png&w=384&q=75">

On Vercel, Image Optimization answers those URLs, and it's metered. One transformation is counted each time the optimizer makes a version it hasn't cached yet (a new image, width, quality or format) or one whose cached copy has expired. At the time of writing, Vercel's pricing page shows the allowance next to the Pro rate:

Vercel's pricing table, Image Optimization rows: transformations are 5K a month included on Hobby and start at $0.05 per 1K on Pro, above the cache read and write rows. (full size, opens in a new tab)
Fig. 2 Hobby gets a fixed monthly allowance; Pro bills by usage instead. Vercel's pricing page, September 2026.

Past the limit, the limits page says new images fail with a 402 and fall back to their alt text, while images already optimized keep working. Two details matter here:

  • The allowance belongs to the team, not to one project. Every project on the team draws from the same pool.
  • Cached versions keep working until they expire, and your original file is untouched, so a page can look half broken.

Confirming the 402 with two requests

The quickest test is to compare the optimized URL with the original file:

# The URL the page requested (copy it from the Network tab)
curl -i "https://example.com/_next/image?url=%2Fwork%2Fproject.png&w=1080&q=75"

# The same file, served straight from /public
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/work/project.png"

On my site, the original file returned 200. The optimized URL returned HTTP 402, with the error code OPTIMIZED_IMAGE_REQUEST_PAYMENT_REQUIRED.

That split rules out the usual suspects. The file exists, the path is right and the deployment is serving it. Only the optimizer says no. Read the pair like this:

Optimized URL Original file What it means
402 200 The allowance is used up
200 200 The server is fine; check the component, its CSS or the src
Fails Fails The file isn't deployed where the page expects it

One allowance for the whole team

When this happened, my Vercel team was on the Hobby plan and hosted more than 20 projects, all sharing one Image Optimization allowance. Once it was used up, new (uncached) next/image requests failed on every project in the team. Motherboard's two logos broke the same way.

With several sites on one team, the site where you notice the problem isn't necessarily the one that used up the pool.

At the time of writing, Vercel's Hobby plan page says you usually wait until 30 days have passed before you can use the feature again. Motherboard's logos came back within a few days, with no new deployment. I don't know exactly when the window resets, so I wouldn't plan around it.

Three ways to fix the 402

Option What changes Trade-off
Turn optimization off One setting in next.config Every image loads at full size
Pre-generate the sizes A sharp script and a <picture> component More files to generate and commit
Upgrade the plan Billing A monthly cost

Turn optimization off

The quickest fix is one setting:

// next.config.ts
const nextConfig: NextConfig = {
  images: { unoptimized: true },
};

With it, Next.js serves each image as-is from its src, with no resizing or format change. The optimizer is out of the path, so the 402s stop.

The catch is size. For screenshots, every visitor downloads the full-size original. Mine show the difference:

Files Source PNG 828 px AVIF 1440 px AVIF
Log Pose screenshot 1.5 MB 54 KB 129 KB
All seven screenshots 3.8 MB 122 KB 281 KB

Small icons barely notice: Motherboard's small icon is a 1.4 KB, 32-pixel PNG, and optimizing it saves less than 1 KB. The same switch exists per image, <Image unoptimized />, which the Next.js docs suggest for images under 1 KB. Icons can skip the optimizer while larger images keep it.

Pre-generate the sizes (what this site does now)

The optimizer's job is to produce a few sizes and modern formats of each image. For a portfolio with a handful of screenshots, that work can happen once, ahead of time.

A small Node script uses sharp to write each screenshot at five widths (480, 640, 828, 1080 and 1440 pixels) in two formats: AVIF at quality 50 and WebP at quality 75. The output goes to public/work:

import sharp from "sharp";

const WIDTHS = [480, 640, 828, 1080, 1440];

// input: the source PNG; slug: its file name without the extension
for (const width of WIDTHS) {
  const base = sharp(input).resize({ width, withoutEnlargement: true });
  await base.clone().avif({ quality: 50 })
    .toFile(`public/work/${slug}-${width}.avif`);
  await base.clone().webp({ quality: 75 })
    .toFile(`public/work/${slug}-${width}.webp`);
}

The original PNGs moved out of /public into a source folder, so they are no longer served.

The component renders a <picture> element and lets the browser choose:

const WIDTHS = [480, 640, 828, 1080, 1440];
const srcSet = (ext: "avif" | "webp") =>
  WIDTHS.map((w) => `${image}-${w}.${ext} ${w}w`).join(", ");

<picture>
  <source type="image/avif" srcSet={srcSet("avif")} sizes={sizes} />
  <source type="image/webp" srcSet={srcSet("webp")} sizes={sizes} />
  <img
    src={`${image}-828.webp`}
    alt={alt}
    width={1440}
    height={900}
    loading="lazy"
    decoding="async"
  />
</picture>
  • The browser uses the first <source> whose type it supports: AVIF where it can, WebP otherwise.
  • srcSet and sizes let it pick the width that fits the screen. On a 1280-pixel laptop screen at 2x, Chrome picks axis-1440.avif for the card below.
  • The <img> is the fallback. Its width and height reserve space before the file arrives.
A project card on cedrickato.com: a framed screenshot of the Axis sign-in page above the card's caption line and its axis-ops.app link. (full size, opens in a new tab)
Fig. 7 The Axis card today, served from axis-1440.avif (the Fig. 01 label is part of the homepage card). In September the same frame and caption rendered without the screenshot.

With no runtime optimizer in the path, the allowance no longer matters for these screenshots. The cost is more files in the repo and one extra step, npm run images, whenever a screenshot changes.

Upgrade the plan

If images change often or come from users, pre-generating them stops being practical. Paying for on-demand optimization is simpler, and next/image keeps working as designed. At the time of writing, Vercel's pricing page lists Pro at $20 a month with $20 of included usage credit, and bills transformations from $0.05 per 1,000 (the pricing rows shown earlier).

There's a second reason to check the plan. At the time of writing, Vercel's fair use guidelines restrict Hobby teams to non-commercial personal use, and they count advertising a product or service as commercial. A site that advertises a paid service needs Pro or Enterprise, whatever its image traffic. This one does, so the team is moving to Pro.

What I check now

  • The console after every deploy. An empty frame is easy to miss. The console error isn't.
  • The team's usage page. Look at the whole team's Image Optimization usage, not one project's. A quota can run out between deploys.
  • The status code before the code. A 402 on /_next/image is a quota problem. Run the two-request check before touching a component.

If you want help with this

I found the cause by mapping the path from the page to the file before changing any code: next/image, Vercel's optimizer and a team-wide allowance. I trained as an architect, and that's how I approach client systems too. If a Next.js app or internal dashboard on Vercel is misbehaving in production, or you want one built with its limits planned from the start, read about the internal tools and dashboards I build on Next.js and Vercel and get in touch from that page.

Written by

Cedric Kato

AI Solutions & Automation Consultant

5+ years building systems for 200+ US-based businesses, and an architecture degree before that.

B.S. Architecture, University of Santo Tomas, 2020

ExperienceLinkedIn (opens in a new tab)

Contact

Tell me what's slowing the business down.

Optional
Optional

I read every message myself.

What happens next

  1. 01I reply personally by email.
  2. 02A short call to walk through your current setup.
  3. 03A written map and a scoped plan, with running costs, before anything is built.
Based in
Metro Manila, PhilippinesGMT+8 · Remote, flexible across US Eastern and AEST hours
Résumé
Download PDF