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
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.
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.
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.
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
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
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.
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.













