Ask
79
@egress_egon ·

$263 of my vercel bill was image optimization, not compute Serverless Pricing

Marketplace site, user-uploaded photos, about 9,000 listings. Bill was $341 and I assumed it was function invocations because traffic doubled last month.

It was not. Functions were $34. Image optimization was $263.

I use the framework image component everywhere with a responsive sizes attribute, which as far as I can tell means each source image can be requested at eight different widths. Multiply by the quality variants and by 9,000 images that are all unique because users upload them, and the transformation count is enormous. These are not the same hero image served a million times, they are a million different images served once each.

What is the actual pattern for user-generated images here? I do not need magic, I need photos to load at a sensible size without paying more for resizing than for hosting.

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @whetstone_wu · 4w ago · 3 replies

    You have correctly identified the fundamental mismatch. Built-in image optimization is priced for a site with a few hundred distinct images requested many times. A marketplace has the opposite shape - many images, each requested a handful of times - so your cache hit rate on transformations is terrible by construction and you pay near full price for almost every one.

    Three things, in order of effort:

    Cut the variant count. Every unique combination of (source, width, quality) is a separate billable transformation. Trim deviceSizes and imageSizes in your config to the widths you actually use, and pin quality to a single value. Going from 8 widths x 2 qualities to 3 widths x 1 quality is a 5x reduction for zero visible difference on a listing card.

    Raise the cache TTL. There is a minimum cache TTL setting and the default is lower than you want for content that never changes. A listing photo uploaded once and never edited should have a TTL measured in months. If the cached transform expires, the next request pays for it again.

    Transform at upload, not at request. This is the real answer for UGC. When the user uploads, generate three sizes once, put them in object storage, store the URLs. Then serve them as plain images with unoptimized set. You pay for three transforms per listing ever, instead of per viewport per visitor. For 9,000 listings that is 27,000 operations total, once.

    66
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @egress_egon · 4w ago

      Transform-on-upload is obviously right for my case and I think I avoided it because it means a queue and a worker. Which, fine, that is an afternoon and it is $263 a month.

      22
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @packlist_pen · 4w ago

      You may not even need a queue. Do it inline in the upload handler with a decent image library - resizing a photo to three widths is a couple hundred milliseconds. Only move it to a queue when the upload latency starts bothering people, or when you add a format conversion that is genuinely slow.

      18
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @kraut_corner · 4w ago · 2 replies

    If you want to keep the image component's ergonomics - the layout behaviour, the lazy loading, the blur placeholder - you do not have to give it up. Point it at your own transform endpoint with a custom loader:

    // next.config.js
    images: { loader: 'custom', loaderFile: './img-loader.ts' }
    

    The loader is a function that takes { src, width, quality } and returns a URL. Return one pointing at your own worker or CDN transform service. Same component everywhere, completely different billing.

    That is the migration path I would take: loader first so nothing in your components changes, then move the actual resizing wherever you want it.

    44
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @durable_ines · 4w ago

      And if you build that endpoint yourself, cache the output aggressively at the edge keyed on the full transform URL. The expensive part is the decode-resize-encode, and the second request for the same size should never reach your code at all.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @dartfit_dana · 4w ago

    Check minimumCacheTTL specifically before doing anything bigger. The default is short, and for immutable user uploads that means you are re-paying for the same transformation on a schedule forever. It is one line and it might halve the number on its own.

    29
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @maren_dowd · 4w ago · 2 replies

    Optimized images are cached permanently after the first request, so this cannot be transformation volume unless your cache is being purged. I would look for a deploy hook or a revalidation call that is clearing the image cache on every deploy.

    11
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @whetstone_wu · 4w ago

      Not permanently - there is a TTL, which is exactly why minimumCacheTTL exists as a setting. And even with an infinite cache, 9,000 unique images times several widths is tens of thousands of first requests, each one billable. Volume alone explains this without needing a purge.

      8
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @packlist_pen · 4w ago

    Also worth confirming what actually counts as billable on your plan - transformations, cached reads, or both - before you optimise the wrong metric. The line item names on the invoice map to specific things and the difference between "how many unique transforms did I create" and "how many optimized images did I serve" changes which fix helps.

    17
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report