Skip to content

Deployment

Terminal window
pnpm run deploy

Builds the Zig WASM, the loader package, and the bundled Next.js demo, then runs wrangler deploy against wrangler.json. The R2 bucket and Durable Object are created automatically on first deploy.

Worker entryWorker bundle (gzip)PlanFormats
src/worker.ts~838 KBFreeJPEG, PNG, WebP, native AVIF

Free plan friendly: 838 KB fits the 1 MB compressed limit. AVIF is on by default. The cold-boot cost over the old 90 KB JPEG/PNG/WebP-only bundle is small (~15–40 ms): wrangler precompiles the libavif WASM at deploy time, and the encoder is only instantiated on the first AVIF request — Workers that never serve AVIF never pay the libavif startup cost.

A single comma-separated env var drops formats at runtime. Recognized values: jpeg, png, webp, avif, gif, svg. Empty / unset = every format enabled. Set in the Cloudflare dashboard (Workers → your Worker → Settings → Variables).

For transformed outputs (jpeg, png, webp, avif), the negotiator skips disabled formats and picks the next-best one the browser accepts. If every format the browser accepts is disabled, the Worker returns 415. For passthrough inputs (gif, svg), the Worker rejects those source types with 415 instead of returning the bytes unchanged.

Typical settings:

  • DISABLED_FORMATS="avif" — drop AVIF first when CPU costs creep. AVIF encode is ~3–4× more CPU than WebP; the negotiator picks WebP, which keeps 60–80% of AVIF’s compression savings.
  • DISABLED_FORMATS="svg,gif" — refuse SVG and animated GIF inputs.
  • DISABLED_FORMATS="avif,webp" — JPEG-only output.
  • DISABLED_FORMATS="webp" — forces JPEG output, useful when a downstream tool can’t read WebP.
  • DISABLED_FORMATS="png" — forces JPEG even when only PNG is acceptable; loses transparency.

The bundled WASM doesn’t shrink either way — encoders simply aren’t instantiated when their format is disabled, so changing the list costs nothing on cold start.

edgesharp uses an R2 bucket to cache transformed images (L2 cache). R2 egress is free, so the only cost on the cache hot path is $0.015/GB-month storage for the cached output you keep around. The bucket is created automatically by Wrangler on first deploy, or you can create it explicitly:

Terminal window
npx wrangler r2 bucket create edgesharp-cache

R2 objects don’t expire by default. To bound storage cost, set up a lifecycle rule:

Terminal window
npx wrangler r2 bucket lifecycle set edgesharp-cache \
--rule '{"id":"expire-30d","enabled":true,"conditions":{"prefix":"v1/"},"expiration":{"days":30}}'

This evicts cached output after 30 days; misses re-run the WASM transform.

Three layers, three ways to clear:

L1 — Cloudflare Cache API. Per-datacenter, transparent, evicts under memory pressure. Not directly purgeable from outside the Worker. Two ways to invalidate: bump the cache-key version (a code change that adds a version segment to buildCacheKey), or purge via CF dashboard → Caching → Purge Everything.

L2 — R2 bucket. Direct control via wrangler:

Terminal window
# List cached objects
npx wrangler r2 object list edgesharp-cache
# Delete one specific object
npx wrangler r2 object delete edgesharp-cache "v1/photo.jpg/w640_q75.webp"
# Delete every cached transform for a single source URL
npx wrangler r2 object list edgesharp-cache --prefix "v1/photo.jpg/" \
| awk 'NR>1 {print $1}' \
| xargs -I {} npx wrangler r2 object delete edgesharp-cache {}
# Nuclear option: empty the whole bucket (irreversible)
npx wrangler r2 bucket delete edgesharp-cache --recursive
npx wrangler r2 bucket create edgesharp-cache

You can also clear via the Cloudflare dashboard: R2 → edgesharp-cache → Objects → select → Delete.

L3 — Source bytes from origin. Not edgesharp’s cache; if you change /photo.jpg at the origin, edgesharp will keep serving the cached transform until L1 + L2 both evict (or you delete the R2 keys above). Bumping the URL (/photo.jpg?v=2) is the cheapest cache-bust on the caller side.

To serve from your own domain instead of *.workers.dev:

  1. Add a custom domain in the Cloudflare dashboard, or
  2. Use a route pattern in wrangler.json:
{
"routes": [
{ "pattern": "images.example.com/*", "zone_name": "example.com" }
]
}

The same Worker URL serves both the bundled Next.js demo (at /) and the /_next/image API. If you only want the API exposed, drop the assets block from wrangler.json.

  • Set ORIGIN to your production source URL
  • List external image hosts in ALLOWED_ORIGINS (or omit to disable absolute URLs)
  • Verify allowed widths match your next.config.js images.deviceSizes and images.imageSizes
  • Decide which formats you want. Defaults: all on. Set DISABLED_FORMATS in the dashboard (comma-separated; recognized: jpeg, png, webp, avif, gif, svg) to drop any of them. DISABLED_FORMATS="avif" is the typical setting when watching CPU spend.
  • If routing AVIF through Cloudflare Images instead, set IMAGE_BACKEND: "cf-images" and bind IMAGES
  • Test with pnpm run dev before deploying
  • Set up an R2 lifecycle rule to bound cache size
  • Update your Next.js images.loaderFile to point at the Worker (see Next.js integration)