How I Serve Images Cheaply Without Vercel Image Optimization
Cloud

How I Serve Images Cheaply Without Vercel Image Optimization

The advice to rip out next/image the day you leave Vercel is wrong, and has been wrong since at least Next 16. Here's what actually costs money, and the four ways to keep it cheap.

Published September 5, 202611 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

Do you actually have to replace next/image when you stop hosting on Vercel?

No. Next.js 16.3.3's own self-hosting docs say image optimization through next/image works self-hosted with zero configuration under next start. The rip-it-out advice is a holdover from posts that never checked. The real cost story is Vercel's per-transformation billing on one side, and sharp's memory behavior on glibc Linux on the other — neither of which requires abandoning next/image, just choosing how to run it.

  • Next.js 16.3.3's self-hosting guide states image optimization works self-hosted with zero config under next start — the widespread 'replace next/image' advice is outdated
  • The one documented real caveat: on glibc-based Linux, sharp may need memory-allocator configuration to avoid excessive memory use
  • Vercel's current image optimization pricing bills per transformation (cache MISS/STALE), not per source image — 5,000 transformations included on Hobby, then $0.05–$0.0812 per 1,000
  • Four real self-hosted options: built-in optimization under next start, a custom loader pointing at Cloudflare Images or an R2 + Worker pipeline, pre-optimizing at build time, and leaning on aggressive CDN caching

Evidence notes

next/image self-hosting behavior

"Image Optimization through next/image works self-hosted with zero configuration when deploying using next start." Sharp caveat: "On glibc-based Linux systems, Image Optimization may require additional configuration to prevent excessive memory usage." Verified against this repo's own node_modules/next/dist/docs/01-app/02-guides/self-hosting.md, Next.js 16.3.3, checked September 2026.

Vercel image optimization pricing

Current default pricing: 5,000 image transformations/month included on Hobby, then $0.05–$0.0812 per 1,000 depending on region. Image cache reads: 300,000/month included, then $0.40–$0.64 per 1M. Image cache writes: 100,000/month included, then $4.00–$6.40 per 1M. This replaced the older per-source-image model. Checked September 2026.

Cloudflare Images pricing

$5 per 100,000 images stored/month, $1 per 100,000 images delivered/month, first 5,000 unique transformations/month included then $0.50 per 1,000 additional. Checked September 2026.

Cloudflare R2 pricing

Standard storage $0.015/GB-month, zero egress fee at any volume. Free tier: 10 GB-month storage, 1M Class A ops, 10M Class B ops. Checked September 2026.

Every "how I moved off Vercel" post has the same paragraph somewhere in the middle: rip out next/image, it's a Vercel-only trick, go build your own pipeline. It isn't true, and checking the current docs takes about ninety seconds. Next.js 16.3.3 — the version this site runs — states plainly that image optimization through next/image works self-hosted with zero configuration the moment you run `next start`. No loader config. No separate image service bolted on. No rewrite. If you deleted next/image from an app before verifying that, you deleted something that already worked.

That one line undoes a surprising number of blog posts, including some written by people who should have checked. But "works with zero config" and "works well on your box under real traffic" are different claims, and the space between them is the actual subject here — because self-hosted image optimization does have a real cost. It's just not the one everyone assumes, and it's not a reason to abandon next/image. The revenue-side version of image optimization is something XenGrowth's growth operations team writes about in more operational detail than I go into here.

Why did this advice ever make sense?

Because for a while, on other platforms, it was true — plenty of static hosts and generic Node deploy targets genuinely gave you nothing for on-the-fly image transformation, so "bring your own pipeline" was the correct answer there. The mistake is applying that answer to a framework feature that has since been documented as working out of the box on the exact deploy command most self-hosted Next.js apps already use. A piece of advice that was right for a different target quietly became folk wisdom repeated for this one, and nobody went back to the source to check whether it still applied.

What actually happens if you keep next/image self-hosted?

Nothing breaks. Optimization runs inside the Next.js server process — each unique combination of source image, requested width, and quality gets decoded, resized, re-encoded, and cached on first request, then served straight from cache on every repeat. On Vercel, that CPU work happens on infrastructure billed by the transformation. Self-hosted, it happens on your own VPS, billed by nothing, until the process is doing enough of it at once to compete with everything else running on that box for CPU and memory.

So where's the actual caveat?

Next.js's own self-hosting guide names it directly, and it isn't about next/image at all — it's about sharp, the library doing the real decode-resize-encode work underneath it. On glibc-based Linux distributions, sharp can need extra memory-allocator configuration to avoid excessive memory use; the docs point at sharp's own install notes for a Linux allocator setting, not a next.config.js flag. This is a system-level tuning concern, the same kind you'd hit running any native image-processing library on Linux, not a defect in Next.js. Skip it, and a page under real image traffic can quietly push the Node process's memory footprint upward on exactly the kind of small VPS this whole self-hosting cluster runs on. There is a longer treatment of the operations side of this in The XenGrowth resource library.

That's the honest version of the caveat. Not "you can't self-host images," but "budget memory for it and configure the allocator, same as any image-processing workload." A much better post than the myth, and a much shorter fix than replacing an entire subsystem.

What else is worth checking in the same docs while you're there?

Two things, both small, both easy to miss. First, the same self-hosting guide recommends running a reverse proxy — nginx or similar — in front of `next start` rather than exposing it directly, specifically so malformed requests, slow-connection attacks, and payload-size limits get handled before they reach the Node process; that's general advice, not image-specific, but it applies directly here because a reverse proxy is also a sane place to add extra caching in front of optimized images. Second, `next/image` exposes a `minimumCacheTTL` option controlling how long an optimized image stays cached, and an `unoptimized` prop that switches a given image off from the optimization pipeline entirely — useful for images you're already serving pre-sized, where running them through sharp again would just burn CPU for no visual benefit.

What is Vercel actually billing you for?

This is the part worth checking before deciding self-hosting is even necessary for your case. Vercel's current image optimization pricing — the default now, replacing an older per-source-image model — bills per transformation, not per image in your project. A transformation is counted on every cache MISS or STALE: a new width, quality, or format combination of the same source image is a new billable event, which is exactly why sites serving many device breakpoints of the same hero image see this line grow fastest of anything on the invoice. XenGrowth on governed AI marketing workflows goes further into AI agents and marketing automation.

Usage

Included (Hobby)

On-demand rate

Image transformations

5,000/month

$0.05–$0.0812 per 1,000

Image cache reads

300,000/month

$0.40–$0.64 per 1,000,000

Image cache writes

100,000/month

$4.00–$6.40 per 1,000,000

Add Fast Data Transfer and Edge Request charges on top for actually delivering the transformed file, and image optimization becomes one of the more opaque line items on a Vercel bill — not because the pricing is hidden, but because nobody can see which image triggered which charge until the invoice already reflects it. That opacity, more than the rate itself, is the actual motivation for looking at self-hosted alternatives.

The cache-read and cache-write meters are worth pausing on, because they're the least intuitive part of this table. A cache write happens once per transformation, when the result first gets stored in Vercel's shared global cache. A cache read happens whenever that cached result needs pulling back from the global cache into a region that hasn't served it recently — not on every hit, only when the local, in-region cache has gone cold. It's a sensible model for a globally distributed CDN with per-region caches; it's also one more moving part you don't have to think about at all once the image work is happening on a single VPS you already control.

Four ways to keep image serving cheap once you're self-hosting

  1. Keep next/image's built-in optimization under next start. Zero configuration, and the CPU/RAM cost lands on your VPS instead of on a metered service. This is the right default for a small-to-medium site — budget the sharp memory tuning above once traffic picks up, not before.

  2. Point a custom loader at Cloudflare Images. Next.js's loaderFile config in next.config.js takes any function that returns a URL, so you can hand transformation off entirely. Cloudflare Images charges $5 per 100,000 images stored per month, $1 per 100,000 delivered, and $0.50 per 1,000 unique transformations beyond the first 5,000 included — a small, predictable per-image bill instead of CPU contention on your own box.

  3. Build an R2 + Worker pipeline. Store originals in R2 — $0.015/GB-month for standard storage, zero egress fee at any volume — and run a resize Worker in front of them that fetches the original, transforms it, and returns the result. More moving parts than a Cloudflare Images loader, because you're writing and maintaining the resize logic yourself instead of renting it, but Workers' free tier (100,000 requests/day) covers a meaningful amount of traffic before you pay anything at all, and you're not locked into Cloudflare Images' specific transformation options if you need something they don't offer.

  4. Pre-optimize at build time and skip runtime transformation entirely. If your image set is mostly static — a blog's cover art, a docs site's screenshots — generate the sizes you actually need once, at build time or via a script, and serve flat files behind long CDN cache TTLs. Zero server-side work per request, at the cost of losing on-demand sizing for an image nobody anticipated needing at that width.

Option

Setup effort

Where the cost lands

Best for

Built-in next/image, next start

None

Your VPS's CPU and RAM

Small-to-medium sites already self-hosting

Custom loader → Cloudflare Images

Low, one loader file

Per-image, metered by Cloudflare

Teams that want zero server-side image code

R2 + Worker pipeline

Medium, you own the Worker

R2 storage plus Worker requests

High image volume, wants control over transforms

Pre-optimize at build + long CDN cache

Medium, a build step

Build time only

Mostly static image sets, low tolerance for runtime cost

Why the CDN in front of any of these still matters

Whichever option you pick, put a CDN in front of it and let it do the boring work. Transformed image URLs are effectively content-addressed by their source, width, quality, and format, so once a given combination has been generated it never needs regenerating — it just needs a cache that holds onto it. Next.js already sets long immutable cache headers on hashed assets; a CDN sitting in front of the app can extend that same behavior to transformed images, so repeat requests for the same size never reach your origin, your Worker, or Cloudflare Images at all. The cheapest transformation is the one that only ever runs once. There is a longer treatment of AI search, GEO and discovery in XenGrowth on building one SEO and GEO content system.

One exception worth naming so nobody applies this post to the wrong deploy target: none of the zero-config behavior above applies to a fully static export. Image Optimization under a static export needs a custom loader defined up front, and images are optimized at request time by whatever service that loader points at rather than during the build — which just means static exports were always going to need option two or three from the list above, not the built-in default. If you're running `next start`, as most self-hosted Next.js apps are, this doesn't affect you at all.

None of these four options require giving up next/image. They're four different answers to "who does the CPU work and who gets billed for it," and the built-in one under next start is still the right starting point for most self-hosted apps.
  • Confirm you're actually running `next start`, not a static export — the zero-config behavior in this post is specific to that deploy mode

  • Check your host's Linux distro for glibc before assuming the sharp memory caveat applies; distros built on musl (like Alpine) don't hit the same allocator issue the docs describe

  • Look at how many distinct width/quality combinations your app actually requests per image before assuming Vercel's per-transformation billing would have been expensive — a site with one responsive breakpoint set is a very different bill than one requesting a dozen

So which one do you actually pick?

Start with the built-in option and only move off it when you've seen a real reason to — a memory ceiling you've hit, or a traffic pattern where offloading transformation genuinely simplifies your ops story. Reach for Cloudflare Images when you'd rather pay a small, predictable per-image bill than think about sharp's memory behavior at all. Reach for R2 plus a Worker when you already run Cloudflare infrastructure and want the transform logic under your own control instead of a third party's. And reach for build-time pre-optimization when your image set barely changes and paying any runtime cost at all, however small, feels like the wrong trade.

The one option that was never actually on the table is the one the myth insists on: throwing away next/image the day you leave Vercel. That advice costs you the type safety, the `sizes` handling, and the layout-shift prevention next/image already gives you for free, in exchange for solving a billing problem you could have solved by choosing a loader instead. If you're running this stack on Next.js with Coolify, next start already has this covered before you write a single line of image-pipeline code — the only decision left is whether your traffic ever gives you a reason to add one.

Further reading from XenGrowth

Where this work meets go-to-market

Working on image optimization inside a commercial team? XenGrowth publishes operator guides on the revenue side of this work.

How should your images be served?

Four questions about volume and where the work should happen. The default answer is simpler than most people expect, and the exceptions are about scale rather than correctness.

1 / 4
Roughly how many distinct images does the site serve?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

next/imageimage optimizationCloudflare ImagesCloudflare R2self-hostingNext.js 16cloud

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

My Complete Self-Hosted Stack for SaaS in 2026

Contabo, Coolify, Docker, Cloudflare, Postgres, R2, Resend, Uptime Kuma, Turborepo and Docker Hub. Here's every piece of the stack I actually run, what each one replaced, and why I picked it over the alternatives.

Navigate

How I Self-Host PostgreSQL for My SaaS (and When I Wouldn't)

Running Postgres in a container is easy. Running it in a way that survives a redeploy, a full disk, and an eventual major-version upgrade is the actual job. Here's the setup, tuned against Postgres's own defaults, and the honest list of where managed wins outright.

Navigate

10 Mistakes That Break a Self-Hosted SaaS

None of these ten show up as a single dramatic outage. They show up as a disk that quietly fills, a rollback that turns out to be impossible, a backup nobody ever restored. Here's the mechanism behind each one, and the fix.

Navigate

Cloudflare R2 vs S3 vs MinIO for SaaS File Storage

The storage price per gigabyte is close enough across all three that it barely matters. Egress is where the decision actually gets made — and one of these three had a rough 2026 that changes the self-hosting math entirely.

Navigate

The Backup Strategy Every Self-Hosted SaaS Needs (3-2-1, Applied)

3-2-1 is easy to nod along to and easy to get wrong in the specific way that only shows up on the day you need it. Here's what it actually means for one VPS running Postgres and Docker volumes, not the generic version you've already skimmed past twice.

Navigate

My Rule for Deciding What to Self-Host and What to Keep Paying For

Self-hosting everything is a bad idea, and I can point to the exact service where I decided that on purpose. Here's the actual rule I use, not a survey of options — and the one counterexample that explains why the rule exists.

Navigate

The Simplest Production Architecture for a Bootstrapped SaaS

One box, a handful of managed pieces around the edges, and a very short list of things you're not allowed to build yet. Here's the architecture, priced out to $20 a month, and the exact signal that tells you when to add each thing you skipped.

Navigate

How I Secure a Fresh VPS Before Deploying Anything

A brand-new VPS gets scanned within minutes of getting an IP address. Here's the exact order I run through before a single container touches the box — and which of these steps are real protection versus which ones are just theatre.

Navigate

vCPU or RAM? How to Size a VPS for Static Sites, Databases and Traffic Spikes

Four readers asked four versions of the same question, and each one has a different honest answer. Static files want RAM for page cache, Postgres wants RAM for shared_buffers, and "slow under traffic" is usually neither CPU nor RAM.

Navigate

Moving a SaaS Off Vercel to a €5.50 VPS: What the Numbers Actually Look Like

Nobody publishes their real Vercel invoice, so most migration posts trade in vibes instead of arithmetic. This one builds the comparison from Vercel's and Contabo's own published rates, states every assumption out loud, and shows exactly where the two lines cross.

Navigate