When You Outgrow the Sharp Endpoint

The Sharp endpoint from Lesson 11 is a perfectly good answer for the third row of the decision matrix. It runs in your SvelteKit origin, transforms CMS images on demand, sets immutable cache headers, and lets a CDN absorb everything after the first hit. For a small-to-medium site, that is the end of the story.

There is a point, though, where it stops being the right tool. Maybe your origin sits in a single region while your traffic is global, and the cold-cache transform requests cross an ocean before the response gets to the user. Maybe your function-execution platform charges by GB-seconds and the 2400-pixel JPEG decodes are eating your budget. Maybe a sudden traffic spike on a freshly published article drives sharp into a CPU storm and the rest of your origin starts timing out alongside it. Maybe your team simply does not want to operate the dependency at all.

That is the moment to consider the fourth row of the decision matrix: a platform-managed image transform service that runs at the CDN edge, transforms images closer to the user, and removes Sharp from your origin entirely.

This lesson is the comparison. Cloudflare Image Resizing, Vercel Image Optimization, Netlify Image CDN, and AWS Lambda@Edge are the four production-relevant options in 2026, each with a slightly different shape but the same core capability: take a remote URL, resize it, transcode it, cache it at the edge. By the end you will have a clear sense of which one fits which deployment target, what the migration from self-hosted Sharp looks like, and the operational tradeoffs that make this a “fourth pipeline” rather than a drop-in replacement for the third.

The Fourth Row, in Plain Terms

Lesson 11’s matrix had three rows: build-time enhanced:img for committed assets, runtime @jsquash for user uploads, server-side sharp for CMS URLs. Each row solved a problem the others could not, and together they covered every realistic image source.

The fourth row is a refinement of the third. Instead of running sharp in a function on your origin, you delegate the entire transform pipeline to a managed service that runs at the CDN edge. The endpoint contract stays similar - give me a URL, optionally a width and a format, and you get back transformed bytes - but the execution model is different. The edge service handles the decode, the resize, the encode, the cache, and the geographic distribution. Your application emits URLs that point at the service; everything downstream is someone else’s operational concern.

The decision matrix becomes:

SourcePipelineLesson
User uploadsClient-side @jsquash04, 06–10
Committed repo assetsBuild-time enhanced:img11
CMS / remote URLs (small site)Server-side sharp endpoint11
CMS / remote URLs (production)Platform edge serviceThis lesson

The third and fourth rows are not mutually exclusive. A self-hosted Sharp endpoint is the cheapest correct answer for sites under a few thousand transforms per day; a platform service is the right answer once the traffic, geography, or operational burden makes the cheap answer expensive. Many teams start at row three and migrate to row four when they hit a specific friction. The migration path is short because the call-site contract - <RemoteImage src=… /> - is identical regardless of what powers it.

Cloudflare: Image Resizing and Cloudflare Images

Cloudflare offers two related products that often get confused: Image Resizing and Cloudflare Images.

Image Resizing is a transform-only service that operates on URLs you already host. You give it a URL pattern and parameters, and it fetches, transforms, and caches the result on Cloudflare’s edge. The pricing is per-million-transforms with a generous free tier. You keep ownership of the original bytes; Cloudflare just runs the transform.

Cloudflare Images is a full storage-and-transform product. You upload originals to Cloudflare’s storage, and every transform is generated on demand from there. Pricing covers both storage and delivery in a single line item.

For a SvelteKit site already storing images in R2 (or any S3-compatible bucket), Image Resizing is usually the better fit. The R2 origin stays the canonical store for the original bytes; the resizing layer is a transform on top. This keeps the storage decision and the transform decision orthogonal.

The integration shape is straightforward. Cloudflare exposes a special URL prefix on your zone (/cdn-cgi/image/<options>/<source>) that triggers the transform pipeline. Replace your RemoteImage URL builder with one that emits these URLs and the rest of the application is untouched.

// src/lib/cms-image.ts - Cloudflare Image Resizing variant
const WIDTHS = [400, 800, 1600] as const
const FORMATS = ['avif', 'webp'] as const

// Cloudflare Image Resizing path:
//   /cdn-cgi/image/width=800,format=webp,quality=80/<source>
// The source must be on the same zone or in an allowlisted origin.
export function buildCfSrcset(remoteUrl: string, format: (typeof FORMATS)[number]) {
	return WIDTHS.map(
		(w) => `/cdn-cgi/image/width=${w},format=${format},quality=80/${remoteUrl} ${w}w`
	).join(', ')
}

The <RemoteImage> component from Lesson 11 needs no other change, it imports buildCmsSrcset and renders. Swapping the import to buildCfSrcset (or branching on an environment flag) is the entire migration on the call side.

The two configuration details that catch teams out:

  • Resize only on enabled zones. Image Resizing must be enabled on the Cloudflare zone serving the request, and your plan must include it. The free tier covers most personal sites; the Pro plan covers most small commercial sites. Checking this before writing the URL builder saves a confusing rollout.
  • Authenticated-fetch origins need a signed token. If your R2 bucket or origin requires authentication, Image Resizing has to be configured with the credentials to fetch from it. Public-bucket setups skip this entirely. The Cloudflare docs cover both modes.

Vercel: Image Optimization

Vercel’s Image Optimization runs as a serverless function attached to every Vercel deployment. It receives a request like /_vercel/image?url=<source>&w=800&q=75, fetches the source, transforms with sharp under the hood, caches at the edge, and returns the bytes.

For SvelteKit projects deployed on Vercel via @sveltejs/adapter-vercel, this is the path of least resistance. The endpoint exists by default, the cache is global, and the transform pricing is metered against the deployment’s plan. There is no integration code beyond emitting the right URL pattern.

// src/lib/cms-image.ts - Vercel Image Optimization variant
const WIDTHS = [400, 800, 1600] as const

export function buildVercelSrcset(remoteUrl: string, _format: 'avif' | 'webp') {
	// Vercel auto-negotiates the format from the Accept header; the `_format`
	// argument is preserved for API parity with the other pipelines but is
	// not actually sent. The `q=75` is a sensible default; tune by content type.
	return WIDTHS.map(
		(w) => `/_vercel/image?url=${encodeURIComponent(remoteUrl)}&w=${w}&q=75 ${w}w`
	).join(', ')
}

The format-negotiation behaviour is the most notable difference from Cloudflare and the Sharp endpoint. Vercel inspects the request’s Accept header and serves AVIF, WebP, or the original format based on what the browser advertises. The srcset therefore does not need separate <source type="image/avif"> and <source type="image/webp"> entries - one srcset is enough, and the browser receives the best-format response per request.

This is convenient but it is also a coupling. The <picture>-element shape from Lesson 8 was deliberately format-explicit; Vercel’s auto-negotiation hides the format choice from your component. For consistency with the build-time and runtime pipelines, the simplest pattern is to emit the explicit <source type=… /> markup anyway and accept that two of the three may resolve to the same actual variant. The cache layer dedupes correctly because both URLs hit the same Vercel-side cache key.

The other quirk worth knowing: Vercel’s allowlist of source domains is configured in next.config.js historically, but for SvelteKit projects on adapter-vercel it lives in the vercel.json images block. Configure it before the first deployment; otherwise the function returns 400 on every request from an unlisted origin.

Netlify: Image CDN

Netlify Image CDN is the newest of the three platform offerings (GA in 2024) and the most opinionated. It exposes a single transform URL pattern (/.netlify/images?url=<source>&w=800&fm=avif&q=80), runs the same fetch–transform–cache cycle, and integrates with Netlify Edge Functions for additional logic.

The capabilities are similar to Vercel’s: format negotiation via the Accept header is supported, explicit fm=avif / fm=webp is supported, and an extensive set of transform parameters (crop, focal point, blur, brightness) is available beyond plain resize.

// src/lib/cms-image.ts - Netlify Image CDN variant
const WIDTHS = [400, 800, 1600] as const

export function buildNetlifySrcset(remoteUrl: string, format: 'avif' | 'webp') {
	return WIDTHS.map(
		(w) => `/.netlify/images?url=${encodeURIComponent(remoteUrl)}&w=${w}&fm=${format}&q=80 ${w}w`
	).join(', ')
}

The notable advantage of Netlify Image CDN is the focal-point support. Manual crop coordinates work fine for most images, but for content where the focal point is the subject’s face or a product centre, a single fp-x and fp-y parameter on the URL keeps the subject in frame across every aspect-ratio crop. This is the kind of feature that a hand-rolled Sharp endpoint can certainly implement, but the cost-benefit only flips in favour of building it yourself once you have a substantial editorial team that needs it.

The pricing model on Netlify is similar to Vercel it is bundled into the deployment plan with overage charges per million transforms. For most projects this is operationally simpler than reasoning about Cloudflare’s per-product line items.

AWS Lambda@Edge

The fourth option is the most flexible and the most operational. Lambda@Edge runs your own code at every CloudFront edge location; combine it with sharp and you have a do-it-yourself version of Image Resizing, hosted entirely on AWS infrastructure.

This path is rarely the right choice for new projects. The Cloudflare and Vercel offerings cover 95% of what teams reach for, with substantially less operational overhead. Lambda@Edge becomes attractive only when there are genuinely AWS-specific reasons: a pre-existing CloudFront distribution that cannot be moved, compliance requirements that demand the transform run in a specific AWS region, or an existing engineering team that already operates Lambda@Edge for other workloads.

The shape of a Lambda@Edge image function is roughly:

// lambda/image-resize/handler.ts (conceptual)
import sharp from 'sharp'
import type { CloudFrontRequestEvent, CloudFrontResultResponse } from 'aws-lambda'

export const handler = async (event: CloudFrontRequestEvent): Promise<CloudFrontResultResponse> => {
	const request = event.Records[0].cf.request
	const params = new URLSearchParams(request.querystring)

	const width = params.get('w') ? Math.min(2400, parseInt(params.get('w')!, 10)) : undefined
	const format = (params.get('f') ?? 'webp') as 'webp' | 'avif' | 'jpeg'

	// Fetch the source from the configured S3 origin via the CloudFront request,
	// transform with sharp, return the bytes with cache headers.
	// (Full implementation runs to ~80 lines with error handling and signed URLs.)
	// ...
}

The pattern works, but the operational burden is real. Layered Lambda packaging, Sharp’s native binary, edge-region cold starts, the IAM dance for S3 access. Every one of these is a thing that breaks once a year and demands attention. The Cloudflare and Vercel paths bury all of this under a managed product. The break-even point is somewhere around “serving more than 10 million transforms per month and already operating an AWS-heavy stack”, which is a small fraction of projects.

Migration: From Sharp Endpoint to Platform Service

If your site already uses the Sharp endpoint from Lesson 11, the migration to a platform service is intentionally small. The <RemoteImage> component from that lesson takes a src prop and renders a <picture> with the appropriate srcset. Swapping the URL builder is the entire change on the call side.

The migration sequence:

  1. Add the new URL builder alongside the existing one. Do not delete buildCmsSrcset until traffic has migrated.
  2. Branch on an environment flag. A simple IMAGE_PIPELINE=cloudflare env var, read in cms-image.ts, picks which builder to call. The component does not know which is active.
  3. Roll out per-route. SvelteKit +page.svelte files import the same RemoteImage, so the rollout is whatever subset of routes you flip the flag for. Start with one low-traffic route, verify the cache-hit ratio reaches steady state, then extend.
  4. Watch the audit. Lighthouse on a representative page should show identical or better LCP and TBT after the switch. If it regresses, the platform service is misconfigured (most commonly a missing origin allowlist) - debug before extending the rollout.
  5. Decommission the Sharp endpoint. Once 100% of traffic is on the platform service, the +server.ts route can be deleted. The sharp dependency can be removed from package.json. The function-region configuration becomes irrelevant.

Throughout the migration, the database row, the upload pipeline, and the LQIP component are unchanged. This is the property the Lesson 8 architecture was designed for: the URL is the integration boundary, and swapping what produces the URL is a contained refactor.

When the Platform Service Is the Wrong Choice

There are three situations where the self-hosted Sharp endpoint from Lesson 11 stays correct.

Tight transform-volume budgets. A personal site with a few thousand image requests per month sits comfortably below every platform’s free tier, and self-hosted Sharp is essentially free at that scale. Switching to a platform service introduces a billing relationship for no real gain.

Strict data residency. If regulation demands every image transform run inside a specific jurisdiction, a managed edge service with global distribution may not satisfy the requirement. Cloudflare’s Workers offer regional routing as an enterprise feature; Vercel and Netlify do not, in 2026. A self-hosted Sharp function in your chosen region is sometimes the only compliant answer.

Custom transform logic the platform does not support. Photo-management products that overlay watermarks, image-editing apps that apply filters server-side, anything that needs algorithmic decisions per request - the platform services are intentionally generic. Self-hosted Sharp gives you arbitrary code at the transform step, which platform services do not. (Cloudflare Workers and Lambda@Edge do, but at that point you are essentially writing a Sharp endpoint that happens to run at the edge.)

For most production sites, none of these apply, and the platform service is the right choice. The point is just that “always use a platform service” is wrong; the decision is contextual.

Common Mistakes and Anti-Patterns

Routing originals through the platform’s storage when you already have a bucket

Cloudflare Images and Vercel Blob both offer storage in addition to transforms. If you already have an R2 / S3 / GCS bucket as the canonical store, do not also upload to the platform’s storage. You end up paying twice and synchronising two stores. Use the platform’s transform-only mode (Cloudflare Image Resizing, Vercel’s images.remotePatterns) so your existing bucket stays the source of truth.

Forgetting the origin allowlist

Every platform service requires an explicit list of origins it will fetch from. Without this, the first request returns 400 (“URL not in allowlist”) and the cache fills with error responses. Configure the allowlist in vercel.json, netlify.toml, or the Cloudflare dashboard before pointing any traffic at the new builder.

Mixing format negotiation strategies

If half your code emits explicit <source type="image/avif"> and the other half relies on Vercel’s auto-negotiation, you will end up with cache fragmentation: the same source produces twice as many cache entries because half are keyed on the explicit format URL and half on the negotiation-result URL. Pick one strategy per project such as explicit <picture> shape, or Accept-header negotiation, and stick with it. The Lesson 8 <picture> markup is a fine choice on every platform; it just means you do not benefit from the auto-negotiation.

Setting q (quality) too high

Platform services default to quality 75–80, which is the right value for most photographic content. Setting q=95 or q=100 “to be safe” doubles the byte size for invisible quality gain, defeats most of the platform’s transform value, and inflates your egress bill linearly. Trust the default; tune downward (to 65 or 70) for highly compressible content like illustrations rather than upward.

Treating the platform’s cache as infinite

Cloudflare Images and Vercel cache responses for a finite duration determined by their internal heuristics, not by your Cache-Control header. Most of the time this is fine, the cache lives long enough that hot items stay cached indefinitely, but a sudden traffic spike on a fresh image can produce more cache misses than you expect during the warm-up window. Pre-warming common widths after a CMS publish webhook (the same trick from Lesson 11) still applies here.

Over-trusting auto-format negotiation

Vercel and Netlify negotiate format via Accept. This works but is opaque: a slightly older browser that advertises image/webp but not image/avif ends up on the WebP branch silently. There is no log line, no audit visibility, no way to confirm coverage from the application side. For projects that care about format-coverage SLAs (large enterprise sites, government accessibility audits), explicit <source> entries with measurable URLs are easier to reason about.

Performance and Scaling Considerations

The latency story shifts when you move from origin Sharp to edge transforms. The Sharp endpoint’s first request from a given region pays origin-round-trip + decode+encode (~150–400ms total); subsequent requests from that region hit the CDN’s regional cache and are near-instant.

The platform services pay the same first-request cost but at the edge location closest to the user, not at your origin. The decode+encode happens regionally and fans out from there. For a single-region origin serving global traffic, this is a substantial win on cold-cache latency, a user in Sydney gets a transform served from a Sydney edge location instead of an origin in Frankfurt.

The cache-hit story is similar but not identical. All three platforms cache aggressively at the edge tier; the per-region cache TTLs vary, and platform-internal cache eviction policies are opaque. A safe assumption is that a hot image stays cached for the lifetime of the deployment plus a grace period; a cold image may cache-miss every few hours in low-traffic regions. This is fine for content sites with predictable traffic and worth measuring on a representative endpoint for sites with spiky traffic.

The cost calculus depends on volume. Cloudflare’s per-million pricing is the lowest at scale; Vercel and Netlify bundle transforms into the deployment plan and charge overages. For a site doing fewer than a million transforms per month, all three are roughly equivalent in cost; for tens of millions, Cloudflare typically wins on price. Lambda@Edge is the most expensive at all scales unless you are already amortising the AWS infrastructure across other workloads.

Conclusion

The fourth row of the decision matrix is the one most production sites eventually graduate to. The self-hosted Sharp endpoint from Lesson 11 is the right cheap answer for small sites; a platform-managed edge transform service is the right answer once traffic, geography, or operational burden push past what a single origin can serve cleanly.

Cloudflare Image Resizing is the strongest fit for sites that already use Cloudflare’s CDN and want a transform-only product layered on top of an existing R2 or S3 origin. Vercel Image Optimization is the path of least resistance for projects on adapter-vercel. Netlify Image CDN is similar for adapter-netlify and adds focal-point support that matters for editorial workflows. AWS Lambda@Edge is the right answer only when there are AWS-specific constraints; for most projects it is more operational burden than the alternatives.

The migration from self-hosted Sharp to a platform service is a contained refactor - swap the URL builder, branch on an env flag for a graduated rollout, decommission the endpoint when traffic has migrated. The <RemoteImage> component, the database schema, and the upload pipeline are all unchanged. This is the architectural reward for the URL-as-integration-boundary design from Lesson 8: the producer of the URL can change without touching anything that consumes it.

The rest of the track stays valid regardless of which row of the matrix powers any individual image. Lesson 14 covers how to test the whole pipeline so it stays green as it grows; Lesson 15 is the audit that proves it.

Key Takeaways

  • The fourth row of the pipeline matrix is a platform-managed edge image transform service: Cloudflare Image Resizing, Vercel Image Optimization, Netlify Image CDN, or AWS Lambda@Edge. It replaces the third row (self-hosted Sharp) once traffic, geography, or operational burden makes the cheap answer expensive.
  • Cloudflare Image Resizing layers transforms on top of your existing R2/S3 origin and is usually the best fit for high-volume sites already on Cloudflare. Cloudflare Images is the storage-included variant; pick one based on whether you already have a canonical store.
  • Vercel Image Optimization is the path of least resistance for adapter-vercel projects. Format negotiation via Accept is automatic; explicit <picture> markup still works and is recommended for cache-key consistency.
  • Netlify Image CDN is similar to Vercel’s offering, with the notable bonus of focal-point support for editorial-heavy sites.
  • AWS Lambda@Edge is rarely the right new-project choice. Pick it only when AWS-specific constraints (existing CloudFront distribution, regional compliance, AWS-amortised team) override the operational simplicity of the managed alternatives.
  • The migration from self-hosted Sharp to a platform service is a URL-builder swap: change buildCmsSrcset, branch on an env flag, roll out per-route, decommission the Sharp endpoint when traffic is migrated. Component, database, and upload pipeline are untouched.
  • Self-hosted Sharp stays correct for small sites, strict data residency, and custom transform logic. The decision is contextual; “always use a platform service” is wrong.
  • Cache-key consistency matters: do not mix explicit format <source> markup with auto-negotiation, or cache fragmentation doubles your transform bill for no quality gain.

Further Reading

See Also