| name | image-resizing-service |
| description | Serve right-sized image variants to mobile via URL-based transforms and a CDN. Use when designing the image delivery path for a mobile app. |
Image Resizing Service
Instructions
Mobile screens need many sizes: thumbnails, feed cards, full-screen, Retina. Shipping originals is wasteful and slow. A resizing service plus a CDN in front gives you bandwidth savings and good cache hit rates.
1. Two Models
A. Pre-generated variants (batch). At upload time, a worker produces a fixed set of sizes and formats. Clients request specific variant URLs.
Pros: low runtime cost, deterministic storage, easy to cache. Cons: you pay for every size whether it is requested or not; adding a new size requires a backfill.
B. On-demand transforms (URL-based). A single origin; clients append transform params; the edge transforms and caches.
Pros: flexible, no backfill. Cons: first-request latency; requires a transform service (self-hosted imgproxy, thumbor, or managed imgix/Cloudinary/Cloudflare Images).
Most mobile apps benefit from a hybrid: pre-generate the handful of sizes used by the primary screens; enable on-demand for long-tail needs.
2. URL Transforms
Design the URL so the client can ask for exactly what the device needs:
https://cdn.example.com/img/med_01HX7.../w=750,h=750,fit=cover,format=auto,q=80
w, h in CSS pixels (client multiplies by device scale).
fit: cover, contain, fill, inside.
format=auto lets the edge pick AVIF / WebP / JPEG based on Accept.
q: quality, default 80; lower for thumbnails.
dpr (device pixel ratio) optional alternative to pre-multiplied w/h.
Allowlist permitted combinations -- attackers will otherwise request every size + format to blow up your cache.
3. Format Selection
- AVIF: best compression; slower decode; ship when device supports it.
- WebP: near-universal on modern mobile; excellent default.
- JPEG: fallback; always the floor.
- HEIC from iOS uploads: transcode to JPEG/WebP for delivery unless the client specifically requests HEIC.
Prefer format=auto and let the edge pick based on Accept headers:
Accept: image/avif,image/webp,image/*;q=0.8
4. Service Implementation
Using imgproxy (Go) behind a CDN:
Client -> CDN -> imgproxy -> object storage
cache hit -> serve directly
imgproxy URL example (signed):
https://cdn.example.com/signature/rs:fill:750:750:1/q:80/format:webp/plain/s3://uploads/med_01HX7/original.jpg
Self-hosted alternative (Python/FastAPI + libvips):
from fastapi import FastAPI, Response
import pyvips
app = FastAPI()
@app.get("/img/{key:path}")
def img(key: str, w: int = 0, h: int = 0, q: int = 80, format: str = "webp"):
im = pyvips.Image.thumbnail(f"s3://uploads/{key}", w or h, height=h or None)
buf = im.write_to_buffer(f".{format}[Q={q}]")
return Response(buf, media_type=f"image/{format}",
headers={"Cache-Control": "public, max-age=31536000, immutable"})
Always process in a streaming, memory-bounded way; libvips is the right default for that. Restrict output dimensions (e.g., max 4096x4096) and decoded pixels to prevent DoS via huge inputs.
5. Cache Strategy
- Immutable URLs (include a content hash or upload id) -> set
Cache-Control: public, max-age=31536000, immutable.
- CDN cache key includes the full transform path.
- Purge rarely; if a user replaces an image, issue a new id rather than mutating.
6. Client Patterns
iOS (SDWebImage / Nuke):
let url = imageURL(mediaId: id, width: view.bounds.width, fit: .cover)
imageView.nuke.load(url)
Android (Coil):
imageView.load(imageUrl(id, widthPx = view.width, fit = "cover")) {
crossfade(true)
placeholder(R.drawable.ph)
}
The helper imageUrl(...) computes device-scaled pixel dimensions and appends transform params. Keep that logic in one place.
7. Placeholders and LQIP
For feed and list views, ship a tiny placeholder (BlurHash string, or a 16x16 WebP) in the JSON payload. The client renders that instantly while the full image streams:
{
"id": "med_01HX7...",
"w": 3024, "h": 4032,
"blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
}
8. Privacy
- Strip EXIF metadata (location, device) on upload or at transform time.
- Private media: sign URLs with a short TTL; do not make buckets public.
9. Cost
- Cache hit rate > 95% is achievable with stable URLs + long max-age.
- Watch egress: mobile image traffic often dominates a backend's total bandwidth bill.
- Compute cost goes up with format=auto + AVIF; measure before broadly enabling.
10. Observability
Track cache hit rate by variant, p95 transform latency on miss, bytes delivered per platform, and format distribution over time. Alert when miss latency degrades -- usually a bad transform service config.
Checklist