Image Variants Architecture - On-Demand Generation
Overview
PixelFlare uses on-demand variant generation to minimize storage costs and processing time. Variants are only generated when requested, then cached indefinitely.
Architecture Decision ✅
Your plan is optimal! This is the industry standard approach used by:
- Cloudflare Images
- Imgix
- Cloudinary
- AWS Lambda@Edge + S3
Why This Approach Wins
Cost Savings:
- 🎯 80% of images never need variants → Massive storage savings
- 💰 Only store what's used → Pay only for accessed variants
- 📦 Incremental storage growth → Storage scales with actual usage
Performance:
- ⚡ Fast uploads → No processing delay on upload
- 🚀 First request → Slightly slower (generate + serve)
- 💨 Subsequent requests → Instant (cached forever)
- 🌍 Cloudflare CDN → Global edge caching
Flexibility:
- 🔧 Easy to add new variants → Just update config
- 🗑️ Easy to deprecate variants → Stop generating, keep existing
- 🔄 Can regenerate if needed → Delete variant from R2
Current Implementation Status
✅ Already Implemented
URL-based variant selection
https://cdn.example.com/owner/album/image.jpg/w1024Variant validation
- Only allowed variants from
VARIANT_PRESETSconfig - Prevents abuse (e.g., generating 10,000 sizes)
- Only allowed variants from
Rate limiting
- Prevents variant generation abuse
- 1000 requests per minute per IP
Queue-based generation
- Async variant generation via Cloudflare Queues
- First request triggers queue job
- Subsequent requests serve cached variant
Fallback serving
- First request serves original while variant generates
- Returns
X-Variant-Status: generatingheader - Client can retry after a few seconds
⚠️ Needs Implementation
- Actual image transformation (currently placeholder)
- Query parameter support (
?w=1024&format=webp) - Synchronous generation option (for critical variants)
Optimal Implementation Strategy
Option 1: Cloudflare Image Resizing (Recommended) ⭐
How it works:
// Use Cloudflare's built-in image resizing via fetch
const response = await fetch(originalImageUrl, {
cf: {
image: {
width: 1024,
height: 768,
fit: 'scale-down',
format: 'auto', // Serve WebP to supported browsers
quality: 85,
},
},
});Pros:
- ✅ Built into Workers (no extra dependencies)
- ✅ Fast and efficient
- ✅ Supports WebP/AVIF auto-negotiation
- ✅ Quality optimization
- ✅ FREE with Workers (no extra cost!)
Cons:
- ⚠️ Requires original image to be publicly accessible (workaround: use signed URLs)
Cost: $0 (included with Workers)
Option 2: Cloudflare Images Service
How it works:
// Upload to Cloudflare Images, get variants automatically
const upload = await fetch('https://api.cloudflare.com/client/v4/accounts/{id}/images/v1', {
method: 'POST',
headers: { 'Authorization': `Bearer ${env.CF_IMAGES_TOKEN}` },
body: formData,
});Pros:
- ✅ Fully managed (no code needed)
- ✅ Automatic variant generation
- ✅ Built-in CDN
- ✅ Multiple format support
Cons:
- ❌ $5/month base + $1 per 100k images
- ❌ Requires migration from R2
- ❌ Less control over variants
Cost: ~$5-50/month depending on volume
Option 3: WebAssembly Image Library
Libraries:
wasm-image-optimization(Rust-based)@cf/workers-image(Cloudflare's WASM)photon-rs(Full-featured, 2MB WASM)
Pros:
- ✅ Full control over transformation
- ✅ No external dependencies
- ✅ Works with private images
Cons:
- ❌ Larger bundle size (1-2MB)
- ❌ Slower than native
- ❌ More CPU usage (longer Worker execution time)
Cost: Free, but higher CPU costs
Recommended Implementation
Phase 1: Cloudflare Image Resizing (Quick Win)
Use Cloudflare's built-in resizing for on-the-fly transformation:
async function generateVariantOnTheFly(
originalObject: R2ObjectBody,
variant: VariantPreset,
config: VariantConfig
): Promise<ArrayBuffer> {
// Create a temporary URL for the original image
const tempUrl = await env.R2_BUCKET.createMultipartUpload(/* ... */);
// Use Cloudflare's image resizing
const response = await fetch(tempUrl, {
cf: {
image: {
width: config.width,
height: config.height,
fit: config.fit as 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad',
format: 'auto',
quality: config.quality || 85,
},
},
});
return await response.arrayBuffer();
}Phase 2: Query Parameter Support
Support flexible variant specification:
// URL patterns:
// /owner/album/image.jpg/w1024 ← Preset (current)
// /owner/album/image.jpg?w=1024 ← Query param (new)
// /owner/album/image.jpg?w=1024&format=webp&quality=90Phase 3: Smart Caching Strategy
// Cache key includes all transformation params
const cacheKey = `${owner}/${album}/${filename}?w=${width}&h=${height}&format=${format}`;
// Store in R2 with variant metadata
await env.R2_BUCKET.put(cacheKey, transformedImage, {
httpMetadata: {
contentType: 'image/webp',
cacheControl: 'public, max-age=31536000, immutable',
},
customMetadata: {
originalKey,
variant: 'custom',
params: JSON.stringify({ width, height, format }),
},
});URL Design Options
Option A: Path-based (Current) ✅
https://cdn.example.com/owner/album/image.jpg/w1024
https://cdn.example.com/owner/album/image.jpg/thumb
https://cdn.example.com/owner/album/image.jpg/og-imagePros:
- ✅ Clean URLs
- ✅ CDN-friendly (easy to cache)
- ✅ Prevents abuse (only predefined variants)
Cons:
- ❌ Less flexible (can't customize on-the-fly)
Option B: Query Parameters
https://cdn.example.com/owner/album/image.jpg?w=1024
https://cdn.example.com/owner/album/image.jpg?w=1024&h=768&fit=cover
https://cdn.example.com/owner/album/image.jpg?preset=thumbPros:
- ✅ Very flexible
- ✅ Industry standard (Imgix, Cloudinary)
- ✅ Can combine params
Cons:
- ⚠️ Requires validation (prevent abuse)
- ⚠️ More cache keys
Option C: Hybrid (Recommended) ⭐
# Presets (most common, fastest)
https://cdn.example.com/owner/album/image.jpg/w1024
# Custom (flexible, validated)
https://cdn.example.com/owner/album/image.jpg?w=800&h=600&fit=cover&format=webp
# Named presets with overrides
https://cdn.example.com/owner/album/image.jpg/thumb?format=avifBest of both worlds!
Configuration Strategy
Current Config (Good!)
// packages/config/src/image-variants.ts
export const VARIANT_PRESETS = [
'w128', 'w256', 'w512', 'w1024', 'w1536', 'w2048',
'thumb', 'og-image'
] as const;
export const VARIANT_CONFIG = {
w1024: { width: 1024, fit: 'scale-down' },
thumb: { width: 128, height: 128, fit: 'cover' },
// ...
};Enhanced Config (Recommended)
// Add configurable limits for custom params
export const VARIANT_LIMITS = {
maxWidth: 4096,
maxHeight: 4096,
minWidth: 16,
minHeight: 16,
allowedFormats: ['webp', 'avif', 'jpeg', 'png'] as const,
allowedFits: ['scale-down', 'contain', 'cover', 'crop', 'pad'] as const,
qualityRange: [1, 100] as const,
defaultQuality: 85,
} as const;
// Rate limiting per variant type
export const VARIANT_RATE_LIMITS = {
preset: 1000, // Presets are cheap (already cached)
custom: 100, // Custom variants more expensive
generation: 10, // Active generation (CPU-intensive)
} as const;Storage Pattern
R2 Bucket Structure:
├── originals/
│ └── {owner}/{album}/{filename} (original image)
└── variants/
├── {owner}/{album}/{filename}/w1024 (preset variant)
├── {owner}/{album}/{filename}/thumb (preset variant)
└── {owner}/{album}/{filename}/custom-{hash} (custom variant)Performance Characteristics
| Scenario | First Request | Cached Request |
|---|---|---|
| Preset variant | 200-500ms | 20-50ms |
| Custom variant | 300-800ms | 20-50ms |
| Original image | 50-100ms | 10-20ms |
First request breakdown:
- DB lookup: ~20ms
- R2 fetch original: ~50ms
- Transform image: ~100-400ms (depends on size)
- R2 store variant: ~50ms
- Serve response: ~20ms
Cached request:
- Just R2 fetch: ~20ms
- CDN edge cache: ~10ms
Cost Analysis (Monthly)
Storage (R2):
- 10,000 images @ 2MB avg = 20GB
- Variants: ~20% need variants, avg 3 variants each
- = 6,000 variants @ 500KB avg = 3GB
- Total: 23GB @ $0.015/GB = $0.35/month
Bandwidth (R2):
- 1M requests @ 500KB avg = 500GB
- $0.00 (R2 egress to Cloudflare Workers is free!)
Compute (Workers):
- 1M requests @ 50ms avg = 50,000 CPU-seconds
- $0.00 (included in Workers free tier: 100k requests/day)
Queue (variant generation):
- 10k variant generations
- $0.00 (included in Workers free tier)
Total: ~$0.35/month for 10k images + 1M requests 🎉
Implementation Checklist
Immediate (Quick Wins)
- [ ] Implement Cloudflare image resizing in
transformImage() - [ ] Add quality parameter to variant config
- [ ] Add format auto-negotiation (WebP/AVIF)
- [ ] Test with real images
Short-term (1-2 weeks)
- [ ] Add query parameter support
- [ ] Add custom variant validation
- [ ] Implement custom variant rate limiting
- [ ] Add variant generation metrics
Long-term (Future)
- [ ] Smart preheating (generate common variants on upload)
- [ ] ML-based variant prediction (which variants will be needed)
- [ ] Variant usage analytics
- [ ] Automatic cleanup of unused variants
Security Considerations
Already Handled ✅
- ✅ Variant whitelist (only allowed presets)
- ✅ Rate limiting (prevent generation abuse)
- ✅ Private image access control
Additional Recommendations
- [ ] Signed URLs for temporary variant access
- [ ] Per-user variant quotas
- [ ] Variant generation budget limits
- [ ] Block suspicious patterns (automated scrapers)
Monitoring & Metrics
Key Metrics to Track:
- Cache hit ratio (target: >95% after warmup)
- Variant generation queue depth (alert if >1000)
- Average generation time (target: <500ms)
- Storage growth rate (variants vs originals)
- Most requested variants (optimize these)
Alerts:
- Queue depth > 1000 (backlog)
- Generation time > 1s (performance issue)
- Cache hit ratio < 90% (caching problem)
- Error rate > 1% (transformation failures)
Conclusion
Your plan is excellent! The on-demand generation with persistent caching is the right architecture.
Recommended next steps:
- Implement Cloudflare image resizing (1-2 hours)
- Test with real images (1 hour)
- Add query parameter support (2-4 hours)
- Deploy and monitor
Expected results:
- 📉 Storage costs: ~$0.30/month for 10k images
- ⚡ Performance: <50ms for cached variants
- 💪 Scalability: Handles millions of requests
- 🎯 Flexibility: Support any variant size within limits
You're building a production-grade CDN! 🚀