Image Resizing Service Low-Level Design

Image Resizing Service — Low-Level Design

An image resizing service generates and serves optimized image variants (thumbnails, responsive sizes, webp) on demand or on upload. It must handle high read volume, storage efficiency, and graceful handling of original source images. This design is asked at Instagram, Cloudflare, and any media-heavy platform.

Two Approaches: Eager vs. Lazy Resizing

Eager (pre-generate on upload):
  When user uploads image → immediately generate all required variants
  (thumbnail 100×100, card 400×300, hero 1200×800, webp versions)
  Pros: zero latency serving; simple CDN caching
  Cons: wasted work if many variants never get requested;
        slow upload response waiting for all variants

Lazy (generate on first request):
  Store only the original; generate variants on first request, then cache
  Pros: only generates what is needed; fast upload
  Cons: first request for any variant is slow (generation time)
  Implementation: Nginx + image processing proxy (imgproxy, thumbor, Cloudflare Images)

Upload Pipeline (Eager Approach)

def handle_image_upload(user_id, file_bytes, filename):
    # Validate
    img = Image.open(io.BytesIO(file_bytes))
    if img.format not in ('JPEG', 'PNG', 'GIF', 'WEBP'):
        raise InvalidFormat()
    if len(file_bytes) > 10 * 1024 * 1024:  # 10MB limit
        raise FileTooLarge()

    # Strip EXIF metadata (privacy: GPS coordinates, device info)
    img = strip_exif(img)

    # Store original
    image_id = generate_uuid()
    original_key = f'images/{image_id}/original.jpg'
    s3.put_object(Bucket='media-bucket', Key=original_key, Body=file_bytes)

    # Queue variant generation
    for variant in REQUIRED_VARIANTS:
        enqueue_resize_job(image_id, original_key, variant)

    db.insert(Image, {
        'id': image_id,
        'user_id': user_id,
        'original_key': original_key,
        'status': 'processing',
    })
    return image_id

REQUIRED_VARIANTS = [
    {'name': 'thumb',  'width': 150,  'height': 150,  'crop': 'fill'},
    {'name': 'card',   'width': 400,  'height': 300,  'crop': 'fit'},
    {'name': 'hero',   'width': 1200, 'height': 630,  'crop': 'fit'},
    {'name': 'avatar', 'width': 64,   'height': 64,   'crop': 'fill'},
]

Resize Worker

from PIL import Image
import io

def resize_image(image_id, original_key, variant):
    # Download original from S3
    obj = s3.get_object(Bucket='media-bucket', Key=original_key)
    img = Image.open(io.BytesIO(obj['Body'].read()))

    # Convert to RGB (PNGs with alpha channel can't be JPEG)
    if img.mode in ('RGBA', 'LA', 'P'):
        background = Image.new('RGB', img.size, (255, 255, 255))
        background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
        img = background

    target_w, target_h = variant['width'], variant['height']

    if variant['crop'] == 'fill':
        # Smart crop: resize to cover, then center-crop
        img = ImageOps.fit(img, (target_w, target_h), Image.LANCZOS)
    else:  # 'fit'
        # Resize maintaining aspect ratio, no crop
        img.thumbnail((target_w, target_h), Image.LANCZOS)

    # Save as JPEG + WebP
    for fmt, ext, quality in [('JPEG', 'jpg', 85), ('WEBP', 'webp', 80)]:
        buf = io.BytesIO()
        img.save(buf, format=fmt, quality=quality, optimize=True)
        key = f'images/{image_id}/{variant["name"]}.{ext}'
        s3.put_object(Bucket='media-bucket', Key=key,
                      Body=buf.getvalue(),
                      ContentType=f'image/{fmt.lower()}')

    db.execute("""
        INSERT INTO ImageVariant (image_id, variant_name, s3_key, width, height)
        VALUES (%(iid)s, %(name)s, %(key)s, %(w)s, %(h)s)
    """, {'iid': image_id, 'name': variant['name'],
          'key': f'images/{image_id}/{variant["name"]}.jpg',
          'w': target_w, 'h': target_h})

Serving via CDN

-- URL structure:
-- https://cdn.example.com/images/{image_id}/{variant}.{ext}
-- CDN origin: S3 bucket
-- CDN caches variants at edge nodes (CloudFront, Fastly, Cloudflare)

def get_image_url(image_id, variant='card', fmt='webp'):
    return f'https://cdn.example.com/images/{image_id}/{variant}.{fmt}'

-- HTML: serve webp with JPEG fallback

  
  ...


-- Cache-Control headers on S3 objects:
-- Cache-Control: public, max-age=31536000, immutable
-- (one year; image variants never change once generated)

Key Interview Points

  • Strip EXIF metadata on upload: Photos from mobile devices embed GPS coordinates, device model, and timestamp. Serving the original image leaks this data. Always strip EXIF before storing or serving user-uploaded images.
  • Store variants as immutable files: Once generated, a variant never changes. Use content-addressed storage (image_id/variant.ext) and set Cache-Control: immutable so CDNs and browsers cache indefinitely.
  • WebP reduces bandwidth 25-35% vs JPEG: Always generate WebP alongside JPEG. Use the HTML picture element with source type=”image/webp” for browsers that support it, falling back to JPEG for those that don’t.
  • Async generation, sync serving: Variant generation takes 100-500ms per image. Never block the upload HTTP response waiting for all variants. Generate asynchronously; serve a placeholder until variants are ready, then switch to the CDN URL.

Image resizing and media processing pipeline design is discussed in Shopify system design interview questions.

Image resizing and media processing system design is covered in Snap system design interview preparation.

Image resizing and listing photo optimization is discussed in Airbnb system design interview guide.

Scroll to Top