Image Transformation Cost And Architecture Spike
Edge Image Transformation: Comparative Cost & Architecture Spike across Fleet Scales
Source Issue: #599
Status:uningested(Canonical research spike & Architectural Decision Record pending/docs distillalignment)
Candidate Targets:docs/architecture/media.md,docs/architecture/IMAGE_AND_MEDIA_MANAGEMENT.md
Target Components & Seams:packages/capabilities/media,packages/capabilities/cms-media,packages/governance,apps/clk-construction,apps/clk-construction-cms
1. Executive Summary & Problem Framing
Client web applications built on SiteSwarm (such as contractor portfolios, artisanal bakeries, boutique medical clinics, and professional services) depend heavily on rich, high-resolution photography. Construction clients like CLK Construction (apps/clk-construction, apps/clk-construction-cms) regularly capture before-and-after project photos using modern mobile devices (iPhone 14/15/16 Pro, Samsung Galaxy S23/S24 Ultra). These devices produce raw photos ranging from 12 megapixels (4032 × 3024) to 48 megapixels (8064 × 6048), yielding uncompressed JPEG or HEIC uploads between 3.5 MB and 18 MB per asset.
1.1 The Real-World Baseline
In SiteSwarm’s current production baseline:
- Master Uploads to R2: High-resolution photography is uploaded via the client CMS (
Worker Binapps/clk-construction-cms) directly into Cloudflare R2 under content-addressed keys (uploads/projects/<uuid>/<filename>). - Zero-Egress Serving: Client frontend applications (
Worker Ainapps/clk-construction) serve assets directly from R2 via proxy routes (/_emdash/api/media/file/<storageKey>). Because R2 charges $0.00 / GB for egress bandwidth, bandwidth bills remain zero regardless of traffic volume. - Zero Layout Shift (0.00 CLS): The CMS persists intrinsic image dimensions (
width,height, and aspect ratio) in D1 at upload time. Frontend templates (apps/clk-construction/src/lib/cms-projects.ts) inject these dimensions into HTML attributes, guaranteeing zero Cumulative Layout Shift. - The Critical Bottleneck: Because edge image resizing has not yet been unified across the platform fleet, client browsers download the raw master asset (often 5 MB - 12 MB) on both desktop and mobile viewports. On a mobile 4G/5G connection, loading a gallery of 10 project photos transfers 50 MB to 120 MB of data, degrading Largest Contentful Paint (LCP) from <1.2s to >4.8s and causing high mobile bounce rates.
1.2 The Platform Scaling Dilemma
To achieve sub-second LCP and Google Lighthouse 100 performance across all devices, images must be delivered in modern formats (WebP and AVIF) resized precisely to the visitor’s viewport width (srcset). However, edge transformations introduce complex unit economics and operational failure risks across multi-tenant fleets.
As SiteSwarm expands from a single client to fleets of 10, 50, 100, and 250+ sites, each hosting between 50 and 5,000 photos, the infrastructure choice between Dynamic On-Demand Edge Transformation (Cloudflare Images / /cdn-cgi/image/), Upload-Time Asynchronous Pre-generation (R2-persisted responsive variants), Client-Side Browser Pre-compression (Canvas/Web Worker), and In-Isolate Compute (Worker-WASM) dictates platform profitability, edge latency, and operational stability.
1.3 Strict Wholesaler Guardrails (Rule 6.1 Compliance)
In strict adherence to SiteSwarm Rule 6.1, this analysis evaluates pure wholesale infrastructure costs (Cloudflare Images SKUs, Cloudflare R2 storage and operation pricing, and Cloudflare Workers compute SKUs). It explicitly avoids prescribing or referencing retail client billing, agency retainers, or customer markups.
2. Cloudflare Images Billing Dynamics & The Transformation Boundary
Understanding the exact billing boundary of Cloudflare’s image optimization suite is essential to prevent unexpected runaway infrastructure costs and tenant-wide service blackouts.
2.1 Definitive Cloudflare SKUs & Pricing Architecture
Cloudflare packages image functionality into distinct commercial and technical mechanisms:
| Product / SKU | Unit Price | Free Allowance (Per Account) | Billing Metric |
|---|---|---|---|
Cloudflare Image Resizing / Transformations (Remote & R2 origins via /cdn-cgi/image/ or Worker binding) |
$0.50 per 1,000 unique transformations | 5,000 unique transformations / month | Unique combination of (Source URL, Transformation Options) per calendar month |
| Cloudflare Hosted Images (Cloudflare-managed storage & delivery) | $5.00 / month per 100,000 stored + $1.00 per 100,000 served | None (Paid add-on only) | Stored image count + HTTP requests delivered |
| Cloudflare R2 Storage | $0.015 per GB / month | 10 GB / month | Gigabyte-months of stored data |
Cloudflare R2 Class A Operations (Writes / Mutations: PutObject, ListBuckets) |
$4.50 per 1,000,000 requests | 1,000,000 requests / month | Mutation operations |
Cloudflare R2 Class B Operations (Reads: GetObject, HeadObject) |
$0.36 per 1,000,000 requests | 10,000,000 requests / month | Read operations |
| Cloudflare R2 Egress Bandwidth | $0.00 / GB (Permanently Zero) | Unlimited | Egress to the Internet or Cloudflare Workers |
2.2 The “Unique Transformation” Billing Boundary
Under Cloudflare Image Transformations, billing is not pegged to edge cache misses or total HTTP requests delivered to visitors. Instead, billing is governed strictly by the number of unique transformations requested within each calendar month.
flowchart TD Req["Incoming HTTP Request\n/cdn-cgi/image/width=640,format=auto/uploads/photo.jpg"] --> EdgeCheck{"Edge Cache Hit\nat Local PoP?"}
EdgeCheck -- "HIT (Within TTL)" --> ServeCached["Serve Cached Asset (0ms Origin)\nTransform Charge: $0.00\nR2 Read Charge: $0.00"]
EdgeCheck -- "MISS (Cold / Evicted)" --> LedgerCheck{"Transformation Tuple Billed\nThis Calendar Month?\n(photo.jpg, w=640, fit=scale-down)"}
LedgerCheck -- "YES (Already Billed in Cycle)" --> ComputeFree["Re-transform at Edge Compute\nTransform Charge: $0.00\nR2 Read: 1 Class B ($0.00000036)"] LedgerCheck -- "NO (First Time in Cycle)" --> QuotaCheck{"Account Monthly\nUnique Transforms < 5,000\nOR Paid Enabled?"}
QuotaCheck -- "NO (Free Quota Exceeded)" --> Err9422["HTTP 403 / cf-resized: err=9422\nBROKEN IMAGE or onerror Fallback"] QuotaCheck -- "YES (Within Quota or Paid)" --> ComputeBilled["Execute Edge Transformation\nLog +1 Unique Transform ($0.0005)\nR2 Read: 1 Class B ($0.00000036)\nPopulate Edge Cache (TTL 31536000s)"]What Constitutes a Unique Transformation Tuple:
A unique transformation is defined by the unique tuple: $$\text{Tuple} = \langle \text{Source URL}, \text{Width}, \text{Height}, \text{Fit}, \text{Quality}, \text{Format}, \text{Gravity}, \text{Metadata}, \text{Rotate}, \text{Sharpen}, \text{Blur} \rangle$$
- Monthly Cycle Reset: At the start of every billing cycle (00:00 UTC on the 1st of each month), Cloudflare resets the unique transformation ledger. The first time a visitor (or search crawler) requests
photo.jpgatwidth=640in the new month, it is counted as a billable unique transformation—even if the asset was requested a million times the previous month. - The
format=autoNegotiation Exemption:- When requests specify
format=auto, Cloudflare inspects the incoming visitor browser’sAcceptHTTP request header and dynamically serves AVIF (to modern Chrome, Safari, Firefox), WebP, or fallback JPEG. - Crucial Architectural Rule: Cloudflare treats
format=autoas a single unique transformation. Serving AVIF to a Mac Safari visitor and WebP to an older Windows Chrome visitor for the same URL and width parameter counts as 1 transformation, not 2.
- When requests specify
- The Multi-Format
<picture>Anti-Pattern:- If frontend code generates HTML with explicit format parameters—such as
<picture><source type="image/avif" srcset="/cdn-cgi/image/width=640,format=avif/photo.jpg"><source type="image/webp" srcset="/cdn-cgi/image/width=640,format=webp/photo.jpg">—Cloudflare registersformat=avifandformat=webpas two distinct unique transformations. - For an application rendering 4 responsive widths across explicit AVIF and WebP sources, every single uploaded photo generates 8 unique transformations ($0.0040 per photo) instead of 4 unique transformations ($0.0020 per photo).
- If frontend code generates HTML with explicit format parameters—such as
- Dimension Quantization & Denial-of-Wallet Vulnerability:
- If an edge application allows arbitrary width inputs (e.g., dynamically accepting
?w=423or dynamically calculating fluid widths on every client resize), a malicious bot or automated crawler can fuzz widths fromw=100tow=2000. - A single catalog of 1,000 photos fuzzed across 200 arbitrary widths generates 200,000 unique transformations = $100.00/month on a single site.
- Mandatory Invariant: SiteSwarm must enforce strict discrete width quantization (e.g.,
DEFAULT_RESPONSIVE_WIDTHS = [360, 640, 1024, 1920]) and reject or clamp all unquantized width requests.
- If an edge application allows arbitrary width inputs (e.g., dynamically accepting
3. Comparative Architecture Evaluation: The 5 Options
We examine five distinct architectural strategies for edge image optimization in SiteSwarm.
graph TD subgraph Option1["Option 1: Dynamic On-Demand Edge (Cloudflare Images)"] O1_Browser["Browser Request"] -->|/cdn-cgi/image/w=640| O1_Edge["Cloudflare Edge Cache"] O1_Edge -->|Miss| O1_Engine["Cloudflare Images Engine\n($0.50/1k transforms)"] O1_Engine -->|Fetch Master| O1_R2["R2 Master Bucket"] end
subgraph Option2["Option 2: Upload-Time Pre-generation to R2"] O2_Upload["CMS Upload"] --> O2_Queue["Media Queue / Worker B"] O2_Queue -->|Generate 4 Widths Once| O2_Gen["Images Engine / WASM"] O2_Gen -->|Persist Variants| O2_R2["R2 Derivative Store\n($0.015/GB/mo, $0 Egress)"] O2_Browser["Browser Request"] -->|Direct URL| O2_Edge["Edge Cache (0 Transform Fee)"] O2_Edge -->|Miss| O2_R2 end
subgraph Option3["Option 3: Client-Side Browser Pre-Compression"] O3_CMS["Client CMS (Browser)"] -->|OffscreenCanvas / Worker| O3_Compress["Downsample & Compress\n(4 WebP Variants in Browser)"] O3_Compress -->|Direct Upload Presigned| O3_R2["R2 Bucket ($0 Transform Fee)"] O3_Browser["Browser Request"] -->|Direct URL| O3_Edge["Edge Cache (0 Transform Fee)"] O3_Edge -->|Miss| O3_R2 end
subgraph Option4["Option 4: In-Isolate Worker-WASM Compute"] O4_Browser["Browser Request"] --> O4_Worker["Edge Worker (V8 Isolate)"] O4_Worker -->|128MB Memory Limit!| O4_Wasm["WASM Image Engine\n(CPU ms & OOM Risk)"] O4_Wasm -->|Stream| O4_R2["R2 Master Bucket"] end
subgraph Option5["Option 5: Hybrid Tier-Gated with Fallback"] O5_Browser["Browser Request"] --> O5_Gate{"Tenant Tier &\nQuota Breaker?"} O5_Gate -- "Enrolled / Under Quota" --> O5_Opt1["Option 1 (Quantized CF Images)"] O5_Gate -- "Err 9422 / Standard Tier" --> O5_Fallback["Direct R2 Raw Passthrough\n(onerror=redirect / Fallback)"] endOption 1: Dynamic Cloudflare Images On-Demand with Width Quantization & Edge Caching
- Mechanics: Images are served via Cloudflare’s edge proxy syntax:
https://<client-domain>/cdn-cgi/image/width=<W>,format=auto,quality=80,fit=scale-down/uploads/<key>Master assets reside in R2. When a visitor requests a quantized width, Cloudflare checks its edge cache. On a cache miss, the Images engine pulls the master from R2, resizes and transcodes the image in memory, caches the derivative at the edge formax-age=31536000, and streams the result to the browser. - Latency & Performance:
- Edge Cache Hit: ~15ms - 35ms (Time-to-First-Byte at closest Anycast PoP).
- Cold Edge Miss / First Transform: ~180ms - 450ms (fetching R2 master + AVIF/WebP edge transcode).
- Resource Footprint:
- Storage: $1 \times \text{master file}$ in R2.
- Compute: Handled entirely by Cloudflare’s multi-tenant image infrastructure; zero V8 isolate CPU time consumed.
- Operational Failure Modes:
- Cloudflare Error 9422 when account monthly quota (5,000 free transforms) is exceeded without an active paid subscription.
- Image failure if master asset exceeds 100 MB or 100,000,000 pixels (100 MP limit).
Option 2: Upload-Time Asynchronous Pre-generation to R2
- Mechanics: When a client uploads a master photo via CMS
Worker B, an asynchronous Cloudflare Queue message (@siteswarm/service-media-pipeline) triggers. The consumer Worker executes a one-time generation of 4 responsive widths (360,640,1024,1920) in WebP (and optionally AVIF), saving each derivative back into R2 under predictable keys:uploads/projects/<uuid>/<filename>-360w.webpuploads/projects/<uuid>/<filename>-640w.webpuploads/projects/<uuid>/<filename>-1024w.webpuploads/projects/<uuid>/<filename>-1920w.webpFrontend templates render direct static URLs to these R2 files. - Latency & Performance:
- Edge Cache Hit: ~15ms - 35ms.
- Cold Edge Miss: ~45ms - 85ms (standard R2
GetObjectstreaming; no on-demand image decoding required).
- Resource Footprint:
- Storage: $1 \times \text{master}$ (~5 MB) + $4 \times \text{derivatives}$ (~0.64 MB total for WebP, or ~1.12 MB for WebP + AVIF) = ~5.64 MB to ~6.12 MB per uploaded photo.
- R2 Class A Writes: 1 master + 4 derivatives = 5
PutObjectcalls per uploaded photo. - Compute: One-time execution per photo at upload time. Zero recurring monthly transformation compute fees.
- Operational Failure Modes:
- Queue backlog during massive bulk photo uploads (e.g., 500 photos uploaded simultaneously by a contractor).
- Storage accumulation over decades if unreferenced media is never garbage-collected.
Option 3: Client-Side Browser Pre-Compression & Resizing at Upload
- Mechanics: Heavy resizing and compression compute is shifted to the client business owner’s browser during the CMS upload flow. When the contractor selects photos in the EmDash CMS admin interface (
apps/clk-construction-cms), a dedicated Web Worker runningOffscreenCanvasandcreateImageBitmap(blob, { resizeWidth: ... })generates the 4 responsive WebP variants locally on the user’s laptop or mobile device. The client then uploads the pre-compressed variants directly to R2 via presigned URLs or CMS Worker B. - Latency & Performance:
- Serving Latency: Identical to Option 2 (~15ms - 35ms edge hit, ~45ms cold miss).
- Upload UX Latency: Resizing 1 photo to 4 widths takes ~120ms - 300ms in modern mobile browsers.
- Bandwidth Uplink Reduction: Instead of uploading an uncompressed 15 MB master over mobile cellular, the browser only uploads compressed WebP variants totaling ~1.2 MB. Upload speed improves by >90% on slow jobsite cell towers.
- Resource Footprint:
- Cloud Infrastructure Cost: $0.00 transformation compute fees permanently.
- Storage: Minimal (~0.64 MB - 1.2 MB per photo if master is compressed to 2560px max width).
- Operational Failure Modes:
- Client device memory pressure: If a contractor selects 50 photos simultaneously on an older smartphone with limited RAM, allocating multiple canvas buffers simultaneously can crash the mobile browser tab. Requires strict single-file sequential queueing.
- Codec variability: Safari on iOS <14 does not support WebP canvas encoding (though 2026 baseline browser support for WebP canvas export is >98.5%).
Option 4: In-Isolate Worker-WASM On-Demand Compute
- Mechanics: Instead of using Cloudflare’s proprietary Images product, a custom Cloudflare Worker compiles a Rust/C image processing library (such as
@photon-rsorwasm-vips) to WebAssembly. The Worker intercepts incoming requests, fetches the master image from R2, decodes the image in WASM memory inside the V8 isolate, downsamples the pixels, encodes the output to WebP, and returns the response. - Latency & Performance:
- Edge Cache Hit: ~15ms - 35ms.
- Cold Edge Miss: 1,200ms - 4,500ms (catastrophic TTFB degradation due to CPU-intensive software rasterization and encoding).
- Resource Footprint & The 128 MB Memory Physics:
- Standard Cloudflare Workers are capped at 128 MB of memory per V8 isolate.
- Memory required to decode an uncompressed bitmap: $$\text{Memory}_{\text{bitmap}} = \text{Width} \times \text{Height} \times 4\text{ bytes (RGBA)}$$
- For a standard 12 megapixel smartphone photo ($4032 \times 3024$): $$4032 \times 3024 \times 4 = 48,771,072\text{ bytes} \approx \mathbf{48.8\text{ MB}}$$
- In addition to the decoded source bitmap, WASM requires an intermediate downsampling buffer ($\approx 11\text{ MB}$ for $1920 \times 1440$) and an output encoding buffer ($\approx 1\text{ MB}$), plus the linear WASM runtime heap ($16\text{ MB}$) and V8 base isolate overhead ($25\text{ MB}$). Total isolate memory: $\approx 102\text{ MB}$ (dangerously close to the 128 MB ceiling).
- For a 24 megapixel camera photo ($6000 \times 4000$): $$6000 \times 4000 \times 4 = 96,000,000\text{ bytes} = \mathbf{96.0\text{ MB}}$$ $$\text{Total Memory} = 96\text{ MB} (\text{source}) + 11\text{ MB} (\text{target}) + 25\text{ MB} (\text{V8}) = \mathbf{132\text{ MB}} \implies \mathbf{OOM\ Crash!}$$
- For a 48 megapixel smartphone photo ($8064 \times 6048$): $$8064 \times 6048 \times 4 = 195,067,904\text{ bytes} \approx \mathbf{195\text{ MB}} \implies \mathbf{Guaranteed\ Immediate\ OOM\ Crash!}$$
- Operational Failure Modes:
- Unrecoverable Worker 1101 crashes (
Worker exceeded memory limit). - CPU limit timeouts (Worker execution halted after 30,000ms CPU time).
- Verdict on Option 4: ARCHITECTURALLY DISQUALIFIED for production multi-tenant master image transformation.
- Unrecoverable Worker 1101 crashes (
Option 5: Hybrid Tier-Gated Delivery with Automatic Fallback
- Mechanics: Combines the on-demand developer ergonomics of Option 1 with hard quota containment.
- Requests route through quantized
/cdn-cgi/image/endpoints with strict parameters (format=auto,onerror=redirect,quality=80, quantized widths[360, 640, 1024, 1920]). - The capability manifest (
packages/governance,swarm.config.ts) gates dynamic transformations by tenant tier. High-volume catalog tenants are automatically shifted to Option 2 asynchronous pre-generation. - Circuit Breaker: If Cloudflare issues
Cf-Resized: err=9422(account quota exhausted) or HTTP 400/500, theonerror=redirectparameter causes Cloudflare edge routers to issue an immediate HTTP 307 redirect to the raw R2 origin URL (/_emdash/api/media/file/<key>), guaranteeing that images never break for visitors even during unexpected quota exhaustion.
- Requests route through quantized
4. Mathematical Modeling & Unit Cost Formulas
To provide rigorous wholesale infrastructure cost comparisons, we define the mathematical model governing monthly costs across all five options.
4.1 Notation & Model Parameters
| Variable | Description | Realistic Baseline Value |
|---|---|---|
| $T$ | Number of client tenant sites in the fleet | $1, 10, 50, 100, 250$ |
| $N$ | Number of active photos per site catalog | $50, 500, 2500, 5000$ |
| $N_{total}$ | Total fleet photo catalog ($T \times N$) | $50 \text{ to } 1,250,000$ |
| $U_{pct}$ | Monthly photo churn / new upload percentage | $10%$ ($0.10 \times N$) |
| $U$ | New photos uploaded per site per month | $5, 50, 250, 500$ |
| $W$ | Number of quantized responsive widths | $4$ ($360, 640, 1024, 1920$) |
| $F$ | Format factor ($F=1$ for format=auto, $F=2$ for explicit AVIF+WebP) |
$1$ (format=auto) |
| $S_{orig}$ | Average master photo storage size in R2 | $5.0\text{ MB} = 0.005\text{ GB}$ |
| $S_{deriv}$ | Total storage size of 4 responsive WebP derivatives | $0.64\text{ MB} = 0.00064\text{ GB}$ |
| $S_{deriv_both}$ | Total storage size of 4 WebP + 4 AVIF derivatives | $1.12\text{ MB} = 0.00112\text{ GB}$ |
| $P_{trans}$ | Cloudflare Images transformation unit cost | $$0.50 \text{ per } 1,000 = $0.0005$ |
| $Q_{free}$ | Free unique transformations included per account/month | $5,000$ |
| $P_{r2_gb}$ | Cloudflare R2 storage unit cost per GB/month | $$0.015$ ($10\text{ GB}$ free/account) |
| $P_{r2_a}$ | Cloudflare R2 Class A operation unit cost | $$4.50 \text{ per } 1,000,000 = $0.0000045$ |
| $P_{r2_b}$ | Cloudflare R2 Class B operation unit cost | $$0.36 \text{ per } 1,000,000 = $0.00000036$ |
| $V_{site}$ | Monthly pageviews per site | $5,000$ |
| $H_{edge}$ | Edge cache hit ratio | $95% = 0.95$ |
4.2 Exact Cost Equations
Option 1: Dynamic Cloudflare Images On-Demand
In an active site where visitors or bots view catalog pages across the 4 responsive breakpoints each month: $$\text{Unique Transforms}{\text{month}} = N{total} \times W \times F$$ $$\text{Cost}{\text{trans}} = \max\left(0, \frac{(N{total} \times W \times F) - Q_{free}}{1000}\right) \times $0.50$$ $$\text{Storage}{\text{GB}} = N{total} \times S_{orig} = N_{total} \times 0.005\text{ GB}$$ $$\text{Cost}{\text{storage}} = \max(0, \text{Storage}{\text{GB}} - 10) \times $0.015$$ $$\text{Cost}{\text{R2_ops}} = (N{total} \times W) \times P_{r2_b} \approx \text{negligible } (< $0.01)$$ $$\mathbf{C}{\text{Option 1}} = \text{Cost}{\text{trans}} + \text{Cost}_{\text{storage}}$$
Option 2: Upload-Time Pre-generation to R2
Derivatives are generated only once upon upload (via worker queue using env.IMAGES.input() or edge pipeline). Existing catalog images in R2 never incur transformation charges again:
$$\text{New Uploads}{\text{month}} = T \times U = T \times (N \times U{pct})$$
$$\text{Cost}{\text{upload_trans}} = \max\left(0, \frac{(T \times U \times W \times F) - Q{free}}{1000}\right) \times $0.50$$
$$\text{Storage}{\text{GB}} = N{total} \times (S_{orig} + S_{deriv}) = N_{total} \times (0.005 + 0.00064) = N_{total} \times 0.00564\text{ GB}$$
$$\text{Cost}{\text{storage}} = \max(0, \text{Storage}{\text{GB}} - 10) \times $0.015$$
$$\text{Class A Writes} = T \times U \times (1 + W) = T \times U \times 5 \text{ ops}$$
$$\text{Cost}{\text{ops}} = \frac{\text{Class A Writes}}{1,000,000} \times $4.50$$
$$\mathbf{C}{\text{Option 2}} = \text{Cost}{\text{upload_trans}} + \text{Cost}{\text{storage}} + \text{Cost}_{\text{ops}}$$
Option 3: Client-Side Browser Pre-Compression & Resizing at Upload
The user’s browser performs downsampling and WebP compression. Furthermore, by downsampling the master photo to a web-optimized 2560px baseline before upload, the stored master size is reduced from 5.0 MB to ~0.50 MB: $$\text{Transformation Compute Cost} = \mathbf{$0.00} \text{ (Offloaded to client device)}$$ $$\text{Storage per Photo} = S_{master_comp} (0.50\text{ MB}) + S_{deriv} (0.64\text{ MB}) = 1.14\text{ MB} = 0.00114\text{ GB}$$ $$\text{Storage}{\text{GB}} = N{total} \times 0.00114\text{ GB}$$ $$\text{Cost}{\text{storage}} = \max(0, \text{Storage}{\text{GB}} - 10) \times $0.015$$ $$\text{Class A Writes} = T \times U \times 5 \implies \text{Cost}{\text{ops}} = \frac{T \times U \times 5}{1,000,000} \times $4.50$$ $$\mathbf{C}{\text{Option 3}} = \text{Cost}{\text{storage}} + \text{Cost}{\text{ops}}$$
Option 4: In-Isolate Worker-WASM On-Demand Compute
Assuming only small images (<2 MP) that do not crash the 128 MB V8 isolate, cold edge cache misses require ~1,200ms of Worker CPU execution time: $$\text{Workers Paid Base} = $5.00/\text{month}$$ $$\text{Cold Miss Transformations} = N_{total} \times W \times 1.0 \text{ (at least once/mo per PoP)}$$ $$\text{CPU ms} = (N_{total} \times W) \times 1200\text{ ms}$$ $$\text{Cost}{\text{cpu}} = \max\left(0, \frac{\text{CPU ms} - 30,000,000}{1,000,000}\right) \times $0.02$$ $$\text{Cost}{\text{storage}} = \text{Cost}{\text{storage (Option 1)}}$$ $$\mathbf{C}{\text{Option 4}} = $5.00 + \text{Cost}{\text{cpu}} + \text{Cost}{\text{storage}} + \text{Severe Latency Penalty}$$
5. Exhaustive Comparative Cost Matrices Across Fleet Scales
The following cost matrices compare Options 1 through 5 across varying fleet sizes ($T = 1, 10, 50, 100$) and catalog sizes ($N = 50, 500, 2500, 5000$ photos per site).
Model Assumptions:
- All costs reflect pure Cloudflare wholesale infrastructure SKUs.
- Monthly photo churn $U_{pct} = 10%$ new photos added per month.
- Width quantization $W = 4$ (
360, 640, 1024, 1920) withformat=auto($F=1$).- Account free allowances applied globally across the account (5,000 free transforms, 10 GB free R2 storage, 1M free Class A ops).
5.1 Single Tenant Deployment ($T = 1$ Site, e.g. CLK Construction)
| Catalog Size ($N$) | Option 1: Dynamic CF Images | Option 2: Upload Pre-Gen (R2) | Option 3: Client Pre-Compress | Option 4: Worker WASM | Option 5: Hybrid Delivery |
|---|---|---|---|---|---|
| 50 Photos (Small Site) | $0.00 (200 transforms < 5k free; 0.25 GB < 10GB free) |
$0.00 (20 upload transforms < 5k free; 0.28 GB storage) |
$0.00 (0 transform fees; 0.06 GB storage) |
$5.00 (Worker base fee) |
$0.00 (Falls within free quota) |
| 500 Photos (Medium Contractor) | $0.00 (2,000 transforms < 5k free; 2.5 GB storage) |
$0.00 (200 upload transforms < 5k free; 2.82 GB storage) |
$0.00 (0 transform fees; 0.57 GB storage) |
$5.00 (Worker base fee) |
$0.00 (Falls within free quota) |
| 2,500 Photos (Large Portfolio) | $2.65 / mo ($2.50 transforms [10k-5k]; $0.15 R2 storage) |
$0.06 / mo ($0.00 upload trans [1k < 5k]; $0.06 storage [14.1GB]) |
$0.00 / mo ($0.00 trans fees; 2.85 GB < 10GB free) |
$5.15 / mo ($5.00 base + $0.15 storage) |
$2.65 / mo (Dynamic transforms active) |
| 5,000 Photos (Heavy Historical) | $7.73 / mo ($7.50 transforms [20k-5k]; $0.23 R2 storage) |
$0.27 / mo ($0.00 upload trans [2k < 5k]; $0.27 storage [28.2GB]) |
$0.00 / mo ($0.00 trans fees; 5.7 GB < 10GB free) |
$5.23 / mo ($5.00 base + $0.23 storage) |
$7.73 / mo (Dynamic transforms active) |
Single Tenant Finding: At $T = 1$, Cloudflare’s 5,000 free monthly transformations absorb the entire dynamic transformation cost for small-to-medium sites ($N \le 1,250$ photos). Cost divergence only begins at $N > 1,250$.
5.2 Small Agency Fleet ($T = 10$ Client Sites)
Total fleet photos = $10 \times N$. Free tier allowance (5,000 transforms, 10 GB) is shared across the entire 10-site fleet.
| Catalog Size ($N$ per site) | Fleet Photos ($N_{total}$) | Option 1: Dynamic CF Images | Option 2: Upload Pre-Gen (R2) | Option 3: Client Pre-Compress | Option 4: Worker WASM (OOM Risk) | Option 5: Hybrid Delivery |
|---|---|---|---|---|---|---|
| 50 Photos | 500 | $0.00 / mo (2,000 trans < 5k free; 2.5 GB storage) |
$0.00 / mo (200 upload trans < 5k; 2.82 GB storage) |
$0.00 / mo (0.57 GB storage) |
$5.00 / mo | $0.00 / mo |
| 500 Photos | 5,000 | $7.73 / mo ($7.50 trans [20k-5k]; $0.23 storage [25GB]) |
$0.27 / mo ($0.00 upload trans [2k < 5k]; $0.27 storage [28.2GB]) |
$0.00 / mo (5.7 GB < 10GB free) |
$5.23 / mo | $0.27 / mo (Tier-gated pre-gen) |
| 2,500 Photos | 25,000 | $48.98 / mo ($47.50 trans [100k-5k]; $1.73 storage [125GB]) |
$4.47 / mo ($2.50 upload trans [10k-5k]; $1.97 storage [141GB]) |
$0.28 / mo ($0 trans; $0.28 storage [28.5GB]) |
$6.73 / mo (+ high OOM failure rate) |
$4.47 / mo (High-volume sites on Opt 2) |
| 5,000 Photos | 50,000 | $100.35 / mo ($97.50 trans [200k-5k]; $2.85 storage [250GB]) |
$11.72 / mo ($7.50 upload trans [20k-5k]; $4.22 storage [282GB]) |
$0.71 / mo ($0 trans; $0.71 storage [57GB]) |
$7.85 / mo (+ high OOM failure rate) |
$11.72 / mo (High-volume sites on Opt 2) |
10-Site Finding: At 10 sites with 500 photos each, Option 1 costs $7.73/mo ($92.76/yr), whereas Option 2 costs $0.27/mo ($3.24/yr)—a 28x cost multiple. At 2,500 photos/site, Option 1 approaches $50/mo ($600/yr) while Option 2 remains under $4.50/mo.
5.3 Growing Multi-Tenant SaaS Fleet ($T = 50$ Client Sites)
Total fleet photos = $50 \times N$. Account-level quota pooling causes instant depletion of free allowances.
| Catalog Size ($N$ per site) | Fleet Photos ($N_{total}$) | Option 1: Dynamic CF Images | Option 2: Upload Pre-Gen (R2) | Option 3: Client Pre-Compress | Option 4: Worker WASM (OOM Risk) | Option 5: Hybrid Delivery |
|---|---|---|---|---|---|---|
| 50 Photos | 2,500 | $2.65 / mo ($2.50 trans [10k-5k]; $0.15 storage) |
$0.06 / mo ($0 trans [1k < 5k]; $0.06 storage) |
$0.00 / mo (2.85 GB storage) |
$5.15 / mo | $0.06 / mo |
| 500 Photos | 25,000 | $48.98 / mo ($47.50 trans [100k-5k]; $1.73 storage [125GB]) |
$4.47 / mo ($2.50 upload trans [10k-5k]; $1.97 storage [141GB]) |
$0.28 / mo ($0 trans; $0.28 storage [28.5GB]) |
$6.73 / mo (+ high OOM failure rate) |
$4.47 / mo |
| 2,500 Photos | 125,000 | $256.73 / mo ($247.50 trans [500k-5k]; $9.23 storage [625GB]) |
$33.08 / mo ($22.50 upload trans [50k-5k]; $10.58 storage [705GB]) |
$1.99 / mo ($0 trans; $1.99 storage [142.5GB]) |
$14.23 / mo (+ catastrophic OOM) |
$33.08 / mo |
| 5,000 Photos | 250,000 | $516.35 / mo ($497.50 trans [1M-5k]; $18.85 storage [1.25TB]) |
$68.83 / mo ($47.50 upload trans [100k-5k]; $21.33 storage [1.41TB]) |
$4.13 / mo ($0 trans; $4.13 storage [285GB]) |
$23.85 / mo (+ catastrophic OOM) |
$68.83 / mo |
50-Site Finding: At 50 sites with 2,500 photos each, dynamic transformation (Option 1) exceeds $250/mo ($3,080/yr). Upload-time pre-generation (Option 2) slashes this by 87% down to $33/mo, while client-side pre-compression (Option 3) eliminates transformation costs entirely, costing just $1.99/mo for pure R2 storage.
5.4 Enterprise Fleet Scale ($T = 100$ Client Sites)
Total fleet photos = $100 \times N$.
| Catalog Size ($N$ per site) | Fleet Photos ($N_{total}$) | Option 1: Dynamic CF Images | Option 2: Upload Pre-Gen (R2) | Option 3: Client Pre-Compress | Option 4: Worker WASM (OOM Risk) | Option 5: Hybrid Delivery |
|---|---|---|---|---|---|---|
| 50 Photos | 5,000 | $7.73 / mo ($7.50 trans; $0.23 storage) |
$0.27 / mo ($0.00 upload trans; $0.27 storage) |
$0.00 / mo (5.7 GB storage) |
$5.23 / mo | $0.27 / mo |
| 500 Photos | 50,000 | $100.35 / mo ($97.50 trans [200k-5k]; $2.85 storage [250GB]) |
$11.72 / mo ($7.50 upload trans [20k-5k]; $4.22 storage [282GB]) |
$0.71 / mo ($0 trans; $0.71 storage [57GB]) |
$7.85 / mo (+ high OOM failure rate) |
$11.72 / mo |
| 2,500 Photos | 250,000 | $516.35 / mo ($497.50 trans [1M-5k]; $18.85 storage [1.25TB]) |
$68.83 / mo ($47.50 upload trans [100k-5k]; $21.33 storage [1.41TB]) |
$4.13 / mo ($0 trans; $4.13 storage [285GB]) |
$23.85 / mo (+ catastrophic OOM) |
$68.83 / mo |
| 5,000 Photos | 500,000 | $1,035.85 / mo ($997.50 trans [2M-5k]; $38.35 storage [2.5TB]) |
$140.23 / mo ($97.50 upload trans [200k-5k]; $42.73 storage [2.82TB]) |
$8.40 / mo ($0 trans; $8.40 storage [570GB]) |
$43.35 / mo (+ catastrophic OOM) |
$140.23 / mo |
100-Site Finding: At scale ($T = 100$, $N = 2,500$ photos), Option 1 inflicts a recurring bill of $516.35/mo ($6,196.20/yr). Option 2 reduces this to $68.83/mo ($825.96/yr)—saving $5,370.24 annually. Option 3 drives the annual infrastructure cost down to $49.56/year.
6. Break-Even Analysis & Sensitivity Curves
The fundamental economic trade-off in edge media architecture is between Recurring Monthly Compute (Dynamic Transforms) vs. Cumulative Permanent Storage (Pre-Generated Derivatives in R2).
xychart-beta title "Monthly Infrastructure Cost vs Fleet Catalog Size (100 Client Sites)" x-axis ["5k Photos", "50k Photos", "100k Photos", "250k Photos", "500k Photos"] y-axis "Cost ($ / month)" 0 --> 1100 line [7.73, 100.35, 204.35, 516.35, 1035.85] line [0.27, 11.72, 24.32, 68.83, 140.23] line [0.00, 0.71, 1.50, 4.13, 8.40](Legend: Blue = Option 1 Dynamic; Orange = Option 2 Upload Pre-gen; Green = Option 3 Client Pre-compress)
6.1 The Storage-to-Transformation Cost Ratio ($119 \times$)
Let us examine the recurring cost to maintain 1,000 photos across 4 responsive widths in a steady-state month with zero new uploads ($U = 0$):
- Option 1 (Dynamic Cloudflare Images):
- 1,000 photos $\times$ 4 widths = 4,000 unique transformations per month.
- At wholesale SKU rate of $$0.50 / 1,000$: $$\text{Cost}_{\text{Option 1 (Compute)}} = 4,000 \times \frac{$0.50}{1,000} = \mathbf{$2.00 / \text{month}}$$
- Option 2 (Pre-Generated Derivatives in R2):
- 4 WebP derivatives per photo = $0.64\text{ MB}$ total derivative storage.
- 1,000 photos $\times 0.64\text{ MB} = 640\text{ MB} = 0.64\text{ GB}$.
- At wholesale SKU rate of $$0.015 / \text{GB} / \text{month}$: $$\text{Cost}_{\text{Option 2 (Storage)}} = 0.64\text{ GB} \times $0.015 = \mathbf{$0.0096 / \text{month}} \approx \mathbf{$0.01 / \text{month}}$$
- Even if 4 WebP + 4 AVIF derivatives are stored ($1.12\text{ MB}$ per photo = $1.12\text{ GB}$): $$\text{Cost}_{\text{Option 2 (Dual Format Storage)}} = 1.12\text{ GB} \times $0.015 = \mathbf{$0.0168 / \text{month}} \approx \mathbf{$0.017 / \text{month}}$$
$$\mathbf{\text{Cost Ratio}} = \frac{\text{Cost}{\text{Dynamic}}}{\text{Cost}{\text{R2 Storage}}} = \frac{$2.00}{$0.0168} = \mathbf{119.05 \times}$$
Dynamic edge transformation compute is 119 times more expensive per month than storing pre-generated derivatives in Cloudflare R2!
6.2 Mathematical Turnover Break-Even Threshold ($U/N$)
At what monthly upload churn rate ($U / N$) does pre-generation lose its economic advantage?
Setting Monthly Cost of Option 1 equal to Monthly Cost of Option 2: $$N \times W \times P_{trans} = (U \times W \times P_{trans}) + (N \times S_{deriv} \times P_{r2_gb})$$ Dividing through by $N \times W \times P_{trans}$: $$1 = \frac{U}{N} + \frac{S_{deriv} \times P_{r2_gb}}{W \times P_{trans}}$$ Substituting the empirical constants:
- $S_{deriv} = 0.00112\text{ GB}$ (Dual format)
- $P_{r2_gb} = $0.015$
- $W = 4$
- $P_{trans} = $0.0005$ $$\frac{S_{deriv} \times P_{r2_gb}}{W \times P_{trans}} = \frac{0.00112 \times 0.015}{4 \times 0.0005} = \frac{0.0000168}{0.002} = 0.0084 \ (0.84%)$$ $$1 = \frac{U}{N} + 0.0084 \implies \frac{U}{N} = 1 - 0.0084 = \mathbf{99.16%}$$
Break-Even Conclusion:
Upload-time pre-generation (Option 2) is economically superior to dynamic transformation (Option 1) as long as the monthly photo upload rate is less than 99.16% of the entire catalog size.
Because small business clients (contractors, bakeries, medical practices) update between 1% and 15% of their photo catalog in any given month, pre-generation into R2 is mathematically guaranteed to be 6x to 100x cheaper across every realistic operational scenario.
7. Operational Failure Modes & Quota Caps: Defenses & Mitigations
Beyond raw cost, each architectural option exhibits specific edge failure modes that threaten platform availability.
sequenceDiagram autonumber actor Visitor as Visitor Mobile Browser participant Edge as Cloudflare Anycast Edge participant ImgEngine as Cloudflare Images Engine participant R2 as Cloudflare R2 Storage (Master)
Visitor->>Edge: GET /cdn-cgi/image/width=640,onerror=redirect/photo.jpg Edge->>ImgEngine: Process Transformation (Cold Miss) Note over ImgEngine: Account Monthly Quota Exceeded! (Error 9422) ImgEngine-->>Edge: HTTP 403 (cf-resized: err=9422)
rect rgb(240, 248, 255) Note over Edge: onerror=redirect Safety Catch Edge-->>Visitor: HTTP 307 Temporary Redirect -> Location: /_emdash/api/media/file/photo.jpg end
Visitor->>Edge: GET /_emdash/api/media/file/photo.jpg (Raw Master) Edge->>R2: GetObject (Zero Egress) R2-->>Edge: 200 OK (5.2 MB JPEG) Edge-->>Visitor: 200 OK (Image Loads Safely, Zero Broken UI!)7.1 Cloudflare Error 9422 (Free unique transformations exhausted)
Failure Anatomy:
When an account reaches its 5,000 unique transformation allowance on the free tier (or if payment processing fails on a paid account), Cloudflare edge nodes immediately stop transforming new image requests.
- HTTP Status: Returns
HTTP 400 Bad RequestorHTTP 403 Forbidden. - Response Header:
cf-resized: err=9422. - Response Body:
ERROR 9422: Free unique transformations by account has been exhausted. - The Silent Cache Hit Trap: Images transformed and cached earlier in the month continue to serve successfully from edge cache. However, newly uploaded photos or newly requested responsive breakpoints fail immediately. This creates baffling, intermittent UI degradation where some gallery cards load while others display broken image icons.
Three-Layer Defense & Mitigation:
- The
onerror=redirectFailsafe: All URLs generated by@siteswarm/mediamust mandateonerror=redirect. When Cloudflare encounters Error 9422, the edge router issues an automatedHTTP 307 Temporary Redirectto the raw un-resized R2 master asset. Although page weight is higher, the user interface never displays broken images. - Dimension Quantization:
Hardcode allowable widths to
[360, 640, 1024, 1920]. Reject any dynamic query parameter that is not a member of this whitelist, preventing bot-driven quota exhaustion. - Account Quota Circuit Breaker:
In
@siteswarm/service-critiqueor platform health monitoring, poll Cloudflare GraphQL analytics for monthly transform volume. When account consumption crosses 85% of quota (4,250 transforms), platform workers toggle the capability flagmedia.dynamicTransformsEnabled = false, gracefully bypassing/cdn-cgi/image/and serving R2 assets directly before hard 9422 errors strike visitors.
7.2 V8 Isolate 128 MB Memory Limits (OOM Crashes in Option 4)
Failure Anatomy:
In Option 4, running WASM decoders in a standard Cloudflare Worker triggers uncatchable V8 fatal errors:
- As proved in Section 3, decoding a $4032 \times 3024$ (12 MP) camera photo requires 48.8 MB of contiguous linear memory for raw RGBA bitmap buffers alone.
- Modern 48 MP smartphone photos ($8064 \times 6048$) require 195 MB of linear buffer space.
- Cloudflare Workers enforce an immutable 128 MB isolate memory ceiling. Attempting to allocate 195 MB triggers:
Fatal error in V8: Isolate out of memory - Process terminated. - The Worker crashes without returning an HTTP response, producing Cloudflare Error 1101 (Worker threw runtime exception).
Mitigation:
Option 4 is formally rejected for production deployment. In-isolate full-size bitmap decoding must be strictly prohibited across SiteSwarm platform workers.
7.3 Client Browser Canvas Memory Exhaustion (Option 3 Defense)
Failure Anatomy:
In Option 3, if a contractor selects 60 high-resolution photos on an iPhone 13 or entry-level Android device, launching 60 concurrent Canvas decoding operations exhausts WebKit/Chromium mobile tab memory limits (typically 256 MB - 512 MB per mobile tab), causing an immediate browser reload (“A problem repeatedly occurred with this webpage”).
Mitigation:
In packages/capabilities/cms-media and client CMS upload handlers:
- Enforce Strict Sequential Processing (Concurrency = 1). Each photo is downsampled and uploaded before the next photo is read from disk.
- Utilize
createImageBitmap(blob, { resizeWidth: W, resizeQuality: "high" })instead of rendering to full-size intermediate<canvas>contexts.createImageBitmapleverages native hardware decoders and frees memory immediately upon WebP export. - Explicitly invoke
canvas.width = 0; canvas.height = 0;after each export to force V8/Blink garbage collection of underlying backing stores.
8. Architectural Decision Record (ADR-0006)
Title
ADR-0006: Hybrid Tier-Gated Image Optimization with Quantized Edge Transformations and Asynchronous R2 Pre-Generation
Status
PROPOSED & RECOMMENDED (Canonical consensus architecture for Epic #598)
Context & Problem Statement
SiteSwarm client applications require responsive WebP/AVIF delivery with 0.00 Cumulative Layout Shift and sub-second Largest Contentful Paint. However, relying purely on on-demand Cloudflare Image transformations (Option 1) across a 100-site fleet costs over $6,100 per year in recurring transformation fees and carries fatal Cloudflare Error 9422 quota exhaustion risks. Conversely, pure client-side compression (Option 3) cannot optimize existing legacy catalogs or API-ingested media. Pure in-isolate WASM (Option 4) violates the 128 MB memory physics of Cloudflare Workers.
Decision
SiteSwarm adopts Option 5 (Hybrid Tier-Gated Delivery with Phase 2 Asynchronous R2 Pre-generation) as the canonical platform media architecture:
flowchart TD Upload["Media Ingestion (CMS / API)"] --> Inspect{"Catalog Size / Tenant Tier"}
Inspect -- "Small Catalog (< 500 Photos) OR New Upload" --> Opt1Path["Quantized Dynamic Transform Path\n/cdn-cgi/image/width=[360,640,1024,1920],format=auto,onerror=redirect/"]
Inspect -- "High-Volume Catalog (>= 500 Photos)" --> QueuePath["Enqueue to @siteswarm/service-media-pipeline\n(Worker B / Queue Consumer)"]
QueuePath --> PreGen["Generate 4 Quantized WebP Derivatives Once\nPersist to R2 (uploads/...-640w.webp)"]
PreGen --> R2DirectPath["Static R2 Direct Serving via Cloudflare Edge Cache\n(0 Transform Fees Recurring, $0.015/GB/mo Storage, $0 Egress)"]
Opt1Path --> ErrCheck{"Cloudflare Error 9422 or Origin 4xx/5xx?"} ErrCheck -- "Yes (Failure)" --> AutoFallback["onerror=redirect HTTP 307\nFallback to Direct R2 Master Asset"] ErrCheck -- "No (Normal)" --> DeliverEdge["Deliver Optimized AVIF/WebP"]- Phase 1: Immediate Baseline Hardening (Epic #598 - Milestone 1):
- Quantize All Breakpoint Widths: Lock
@siteswarm/mediatoDEFAULT_RESPONSIVE_WIDTHS = [360, 640, 1024, 1920]. Reject arbitrary width inputs. - Enforce
format=autoInvariant: Eliminate explicit multi-format<picture>branches that double transformation bills. Rely on Cloudflare’s single-chargeformat=autobrowser negotiation. - Mandate
onerror=redirect: Appendonerror=redirectto all/cdn-cgi/image/URL strings, guaranteeing zero broken images on Error 9422 or origin failures. - Update CLK Construction Baseline: Refactor
apps/clk-construction/src/lib/cms-projects.tsto output quantizedsrcsetstrings usingbuildCloudflareImageUrl().
- Quantize All Breakpoint Widths: Lock
- Phase 2: Asynchronous R2 Pre-generation Pipeline (Epic #598 - Milestone 2):
- Introduce
@siteswarm/service-media-pipeline(or CMS Worker B background queue hook). - For catalogs exceeding 500 photos, asynchronously pre-generate 4 responsive WebP derivatives upon upload and persist them into R2.
- Deliver derivative assets directly from R2 via Cloudflare edge cache with $0.00 recurring transformation fees.
- Introduce
- Phase 3: Client-Side CMS Pre-Compression (Epic #598 - Milestone 3):
- Add a sequential Web Worker in
@siteswarm/cms-mediaand client CMS plugins (apps/clk-construction-cms/src/plugins/clk-gallery-admin.tsx). - Downsample massive 15 MB mobile camera uploads to a 2560px WebP master before transmission, reducing R2 master storage consumption by >85% and speeding up mobile upload times over cell connections by >90%.
- Add a sequential Web Worker in
Consequences
Positive Consequences:
- Massive Cost Reduction: Reduces monthly platform infrastructure bills from $516.35/mo down to $68.83/mo (Phase 2) and $4.13/mo (Phase 3) for a 100-site fleet.
- Zero Broken Images Guarantee: The combination of
onerror=redirectand R2 fallback prevents Error 9422 from ever rendering a broken image to visitors. - 100% Wholesaler Alignment: Leverages Cloudflare’s strengths (R2 zero egress and global Anycast edge caching) while bypassing Cloudflare’s high-margin recurring transformation fees.
- Sub-Second Performance: Delivers modern WebP/AVIF imagery across all viewports with 0.00 CLS.
Negative Consequences:
- Storage Multiplier: Storing 4 WebP derivatives in R2 increases per-photo storage from 5.0 MB to 5.64 MB (+12.8% storage volume). However, at $0.015/GB/mo, storing 10,000 photos costs only $0.85/month.
- Queue Pipeline Complexity: Requires managing an asynchronous queue consumer for upload-time processing in Phase 2.
Neutral Consequences:
- Client CMS upload UI requires an asynchronous “Processing responsive sizes…” indicator or optimistic publishing state.
9. Phased Implementation Roadmap for Epic #598
gantt title Epic #598 Implementation Roadmap dateFormat YYYY-MM-DD section Phase 1: Baseline Hardening Quantize widths & enforce format=auto in @siteswarm/media :done, p1_1, 2026-11-05, 3d Add onerror=redirect default to buildCloudflareImageUrl :done, p1_2, 2026-11-07, 2d Wire quantized srcset into CLK Construction (apps/clk) :active, p1_3, 2026-11-08, 4d section Phase 2: Asynchronous R2 Pre-Gen Build @siteswarm/service-media-pipeline Queue Consumer :p2_1, 2026-11-12, 7d Implement R2 derivative storage key schema :p2_2, 2026-11-15, 4d Update media resolution adapter to prefer R2 derivatives :p2_3, 2026-11-19, 5d section Phase 3: Client CMS Pre-Compression Implement sequential Web Worker OffscreenCanvas in cms-media :p3_1, 2026-11-24, 6d Integrate client pre-scaler into clk-gallery-admin.tsx :p3_2, 2026-11-28, 4dMilestone Breakdown
Milestone 1: Defensive URL Contracts & CLK Construction Integration
- Target Seams:
packages/capabilities/media/src/index.ts,apps/clk-construction/src/lib/cms-projects.ts - Deliverables:
- Update
buildCloudflareImageUrl()inpackages/capabilities/mediato defaultonerror: "redirect". - Deprecate explicit
format: "avif"/format: "webp"ingeneratePictureSources(); mandateformat: "auto"responsivesrcsetgeneration to avoid double transformation fees. - Wire
generateResponsiveSrcSet()intoapps/clk-construction/src/lib/cms-projects.ts:toPhoto(), upgrading CLK Construction from raw 15 MB photo delivery to responsive 360/640/1024/1920 WebP/AVIF delivery. - Add unit test assertions in
packages/capabilities/media/tests/media.test.tsverifying width quantization clamping andonerror=redirectserialization.
- Update
Milestone 2: Asynchronous R2 Derivative Pre-Generation Pipeline
- Target Seams:
services/media-pipeline(orpackages/capabilities/cms-media/src/handler.ts) - Deliverables:
- Define derivative R2 key grammar:
uploads/<collection>/<uuid>/<filename>.<width>.webp. - Implement queue consumer Worker utilizing
env.IMAGES.input()or headless transform binding to write 4 derivatives on upload. - Provide fallback resolution in
@siteswarm/cms-mediaadapter: if derivative exists in R2, return direct R2 CDN URL; otherwise fall back to quantized/cdn-cgi/image/URL.
- Define derivative R2 key grammar:
Milestone 3: Client-Side Canvas Pre-Compression in EmDash CMS
- Target Seams:
packages/capabilities/cms-media,apps/clk-construction-cms - Deliverables:
- Create
packages/capabilities/cms-media/src/client-compressor.tsfeaturing sequentialcreateImageBitmapworker scaling. - Integrate into EmDash media upload modal, pre-scaling images to max 2560px before uploading to R2.
- Validate memory safety on iOS WebKit and Android Chrome.
- Create
10. Candidate Distillation Updates for Canonical Architecture Docs
The findings from this spike will be distilled into docs/architecture/IMAGE_AND_MEDIA_MANAGEMENT.md and docs/architecture/media.md. Below are the concrete, PR-ready updates:
Proposed Addition to docs/architecture/IMAGE_AND_MEDIA_MANAGEMENT.md:
### 3. Edge Transformation Unit Economics & Failure Containment
1. **Width Quantization & Billing Bounds**: - All edge image transformation URLs must strictly adhere to discrete width quantization: `[360, 640, 1024, 1920]`. Arbitrary dynamic widths are rejected to prevent bot-driven unique transformation explosions. - Modern format negotiation must use `format=auto`. Explicit multi-format `<picture>` sources are prohibited because they double billable transformations per asset.2. **Fail-Safe Fallback (`onerror=redirect`)**: - Every `/cdn-cgi/image/` URL generated by `@siteswarm/media` includes `onerror=redirect`. If Cloudflare Error 9422 (monthly transformation quota exhaustion) occurs, Cloudflare edge routers issue an HTTP 307 redirect to the raw R2 master, guaranteeing zero broken images.3. **Hybrid Pre-Generation for High-Volume Catalogs**: - Catalogs exceeding 500 photos transition from dynamic on-demand transformation to upload-time asynchronous R2 pre-generation. Pre-generating 4 responsive WebP variants into R2 costs $0.015/GB/mo storage ($0.01/1k photos/mo) versus $2.00/1k photos/mo for recurring dynamic transformations—a 119x infrastructure cost efficiency.Decisions Intentionally Deferred:
- Automated AVIF Pre-generation: While WebP pre-generation is universally supported with fast encoding, AVIF encoding in Worker WASM is CPU-prohibitive. On-demand dynamic AVIF via Cloudflare
format=autois preserved for modern browsers until native edge AVIF hardware encoders become available in Workers. - Client-Side Heavy Watermarking: Watermarking remains in edge workers rather than client canvas to prevent client-side inspection or tampering.