Design an image upload and processing pipeline
The interviewer asks
“Design the image pipeline for a social-commerce app like Meesho or Instagram: users and sellers upload photos from phones, the system validates, moderates and resizes them, and the images are shown quickly in feeds and product pages on slow networks.”
What a strong answer looks like
A strong answer keeps big bytes away from application servers: the client uploads directly to object storage with a pre-signed URL, and an object-created event starts an asynchronous pipeline. Workers validate the file (real type, dimensions, decompression bombs), strip EXIF location data, fix orientation, compute a perceptual hash for duplicate detection, run a moderation model, and produce a few standard variants in modern formats (WebP/AVIF with JPEG fallback). Metadata and status live in a database; the client learns the result by polling or push. Delivery goes through a CDN, with either pre-generated variants or an on-the-fly resize service behind the CDN for less common sizes. The deep dives are direct upload security, the idempotent processing pipeline with retries and a dead-letter queue, pre-generate vs on-demand resizing, and moderation without blocking good uploads.
Concepts to mention
1 Requirements
Functional
- Upload one or many images (up to 20 MB each) from mobile and web, resumable on bad networks
- Validate type and size; reject corrupt, malicious or oversized files
- Strip EXIF metadata (GPS location) and fix orientation
- Generate variants: thumbnail, feed, full-screen, zoom, in WebP/AVIF plus JPEG fallback
- Moderate for nudity, violence and prohibited products; send unclear cases to human review
- Detect duplicate or near-duplicate images (for example the same product photo reused by many resellers)
- Serve images fast with the right size and format for each device; let owners delete images
Non-functional
- ~20M uploads/day (peak ~1,000/s during sale events), ~2B image views/day
- Upload acknowledged quickly; processed variants ready < 10 s p95 after upload
- Image load from CDN < 200 ms p95 in India; CDN hit ratio > 95%
- No uploaded original is lost (object storage durability); processing is retried until success or sent to a dead-letter queue
- Never serve an image publicly before moderation passes (for public surfaces)
- Deleted images stop being served within minutes (CDN purge)
Out of scope (say it out loud)
- Video uploads and transcoding
- Photo editing filters in the app
- Visual search (search by image) beyond mentioning embeddings
- Detailed moderation model training
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/images/uploads | Request upload slots: {count, content_types, sizes} → [{image_id, presigned_url or multipart upload id, expires_at}] |
PUT<presigned S3 URL> | Client uploads bytes directly to object storage (multipart for > 5 MB) |
POST/v1/images/{id}/complete | Optional client confirmation; the pipeline also starts from the storage event |
GET/v1/images/{id} | Status (uploaded/processing/ready/rejected) and variant URLs |
GEThttps://img.cdn…/{id}/{variant}.{webp|avif|jpg} | CDN delivery of a pre-generated variant |
GEThttps://img.cdn…/{id}?w=360&q=75&fmt=auto&sig=… | Signed on-demand resize for less common sizes |
DELETE/v1/images/{id} | Delete image: mark deleted, purge CDN, remove objects asynchronously |
4 Data model
imagesPostgres (or DynamoDB)image_id (PK, ULID)owner_idstatusoriginal_keywidthheightmimebytessha256phash (64-bit)moderation_statusvariants (JSON)created_atdeleted_atIndex on owner_id; index on sha256 for exact dedup. Status drives what the app shows.
originalsS3 private bucketkey = originals/{image_id}Never publicly served. Lifecycle rule moves to infrequent-access after 30 days; incomplete multipart uploads aborted after 1 day.
variantsS3 public-read via CDN onlykey = v/{image_id}/{variant}.{fmt}Immutable keys; long cache headers (1 year). A changed image gets a new id or version suffix.
processing jobsSQS / Kafka + DLQimage_idattemptstageerrorOne message per uploaded object; failed messages after N attempts go to a dead-letter queue for inspection and replay.
phash indexRedis / vector index (BK-tree or LSH buckets)phash bands → image_idsNear-duplicate lookup by Hamming distance; used for catalogue dedup and to block re-uploads of banned images.
moderation_reviewsPostgresimage_idmodel_scoresdecisionreviewer_iddecided_atHuman review queue for borderline scores and appeals.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Seller uploads 5 product photos
- 1The app downsizes photos on the device to at most ~2048-4096 px and converts HEIC to JPEG if needed, cutting upload size on mobile data.
- 2It calls POST /images/uploads for 5 slots; the upload service checks the user's quota and rate limit, creates 5 rows with status=pending, and returns pre-signed PUT URLs valid for 15 minutes, restricted to the expected content type and max size.
- 3The app uploads each file directly to S3 (multipart for larger files, so a dropped connection resumes per part).
- 4S3 emits ObjectCreated events into the queue; one message per image.
- 5A worker sets status=processing, verifies magic bytes and dimensions, decodes safely with pixel limits, strips EXIF, applies orientation, and computes sha256 and a perceptual hash.
- 6It checks the phash index: an exact or near duplicate of a banned image is rejected; a duplicate of an existing catalogue image is linked instead of stored again.
- 7It runs the moderation model; scores above the block threshold reject the image, borderline scores go to human review, and the rest pass.
- 8It encodes variants (150, 360, 720, 1440 px wide) in WebP and AVIF plus a JPEG fallback, writes them under immutable keys, and updates the row to ready with the variant URLs.
- 9The app, polling /images/{id} or receiving a push, shows the processed images.
A buyer scrolls the feed
- 1The feed API returns image ids and a URL template; the app picks the variant for its screen width and density (e.g. 360 px on a budget phone).
- 2The app requests the image with Accept: image/avif,image/webp; the CDN serves the best format it has cached for that Accept header (Vary on format).
- 3On a miss the CDN fetches the pre-generated variant from S3, or calls the on-demand resizer for an uncommon size, which reads the original, resizes, and returns with long cache headers.
- 4Placeholders (a tiny blurred preview or dominant colour stored in metadata) are shown while the real image loads on slow networks.
7 Deep dives
Direct-to-storage uploads, done safely
Proxying 3 MB uploads through API servers wastes their memory, bandwidth and threads, and a slow 2G client can hold a connection for a minute. Pre-signed URLs let the client upload straight to object storage while the API stays small. The URL must be tightly scoped: one object key chosen by the server (never by the client), a short expiry (5-15 minutes), and a content-length range and content type in the signature (S3 POST policies support size limits). Use multipart upload for files over ~5-10 MB so a failed part is retried alone. The server still treats the object as untrusted: the content-type header can lie, so workers check magic bytes and decode with strict limits on pixel count to stop decompression bombs (a tiny PNG that expands to 50,000 × 50,000 pixels and exhausts memory). Rate-limit upload slots per user and per IP so a bot cannot fill your bucket. Incomplete multipart uploads and pending rows that never complete are cleaned up by lifecycle rules and a sweeper job.
An idempotent, retryable processing pipeline
Treat the pipeline as at-least-once: storage events can be delivered twice, workers crash mid-job, and the moderation API times out. Make every step idempotent. Output keys are deterministic (v/{image_id}/360.webp), so a retry simply overwrites with the same bytes. Status changes use conditional updates (set ready only if status is processing and version matches), so a slow duplicate worker cannot overwrite a newer result such as rejected. Use a visibility timeout longer than the worst-case job time, and extend it for large images. Split slow and fast work if needed: a quick stage that validates, strips EXIF and creates a thumbnail (so the user sees something within 2-3 seconds), and a heavier stage that produces AVIF and zoom variants. After N failed attempts, move the message to a dead-letter queue with the error, alert on DLQ size, and provide a replay tool. Workers autoscale on queue depth; during a sale, a backlog grows harmlessly instead of failing uploads.
Pre-generate variants vs resize on demand
Pre-generating a fixed set of variants makes delivery trivial (static files, perfect caching) but wastes storage on variants nobody requests and requires a backfill job whenever designers add a new size. On-demand resizing (an imgproxy or Thumbor-style service behind the CDN) creates any size from the original when first requested and lets the CDN cache it, so new sizes need no backfill; the cost is CPU on cache misses and a risk of abuse, since attackers can request endless size combinations to burn CPU and pollute the cache. Protect it by signing URLs (the app gets a template with an HMAC) or by allowing only a fixed list of widths. A common hybrid: pre-generate the 2-3 variants that serve 90% of traffic (feed and thumbnail) at upload time, and use on-demand resizing for the rest. Put an origin shield in front of the resizer so each new variant is generated once, not once per CDN edge. Format negotiation (AVIF where supported, else WebP, else JPEG) usually saves 30-50% bytes versus JPEG, which matters on Indian mobile networks.
Moderation and duplicates without blocking everyone
Moderation must sit before public visibility but should not make every upload slow. Run a fast classifier (NSFW, violence, weapons, prohibited items) inside the pipeline; it typically takes tens of milliseconds on GPU or a bit more on CPU. Use two thresholds: above the high one, reject automatically; between thresholds, publish to the owner only and queue for human review; below, publish. Track the review queue size and the share of uploads it receives, because a model update that doubles borderline cases will swamp reviewers. Perceptual hashes (pHash, dHash) map visually similar images to hashes with small Hamming distance, unlike SHA-256 which changes with one pixel. Use them to block re-uploads of images already removed, and in commerce to cluster the same product photo uploaded by hundreds of resellers, which helps catalogue quality and saves storage. For scale, index hashes with LSH bands or a BK-tree, or use image embeddings in a vector index when you need semantic similarity.
8 Trade-offs
Presigned direct upload vs proxy through API
Direct upload scales without app servers handling bytes and is cheaper. Proxying gives simpler validation before storage, but ties up servers on slow clients. Direct upload plus asynchronous validation is the standard choice.
Synchronous vs asynchronous processing
Processing in the upload request gives an instant final result but makes uploads slow and fragile under load. Asynchronous processing with a fast first stage gives a quick thumbnail and absorbs spikes in the queue.
Keeping originals
Keeping originals costs storage but lets you regenerate variants for new sizes or formats (for example when adopting AVIF). Move them to cheaper storage classes instead of deleting them.
Client-side downscaling
Resizing on the phone before upload cuts bytes and upload time a lot on mobile networks, at the cost of not having the full-resolution original. For commerce and social apps that is almost always fine.
9 Bottlenecks
Sale-day upload spike
Sellers upload catalogues before a big sale. The queue absorbs the spike; scale workers on queue depth and keep AVIF generation in a lower-priority stage so thumbnails stay fast.
CDN cache misses on a new viral image
Thousands of edges miss at once. An origin shield with request collapsing turns them into one fetch from S3 or the resizer.
Moderation GPU capacity
Batch requests to the model, autoscale GPU workers on queue depth, and fall back to 'visible only to owner until reviewed' rather than failing the upload.
10 Failure handling
Worker crashes mid-processing
The message reappears after the visibility timeout and another worker redoes the job; deterministic output keys make the retry safe.
Moderation service down
Images stay in 'pending review' and are visible only to the owner; the pipeline retries with backoff. Do not fail open for public surfaces.
Client disconnects during upload
Multipart upload resumes the missing parts with a refreshed URL; abandoned uploads are cleaned up by lifecycle rules and a sweeper that expires pending rows after 24 hours.
A poison image keeps crashing workers
After N attempts it goes to the DLQ instead of looping forever; strict decode limits and running decoders in a sandbox stop one file from taking down the fleet.
11 What impresses
- Keeps image bytes off application servers using scoped pre-signed URLs
- Mentions decompression bombs and EXIF location stripping as security and privacy concerns
- Designs every pipeline step to be idempotent and explains the DLQ
- Compares pre-generation with on-demand resizing and protects the resizer from abuse with signed URLs
- Uses perceptual hashes for duplicates and banned re-uploads
- Thinks about Indian mobile networks: client downscaling, small variants, AVIF/WebP, placeholders
12 Mistakes
- Uploading through the API servers and processing synchronously in the request
- Trusting the client's content-type or file extension
- Serving originals directly to every device
- Mutable image URLs that require CDN purges for every edit
- No retry or dead-letter strategy for failed processing
- Publishing images before moderation
13 Follow-ups
Lessons this design uses
Practice questions
- A teammate stores user-uploaded product photos as BLOBs in Postgres. Is that a good idea? What would you do?
- Your product images and JavaScript bundles are served directly from your app servers, and they now use 60% of your bandwidth. What would you change?
- Users of your app upload profile videos of up to 200 MB. Right now the files go through your API servers to S3, and the API pods keep running out of memory. How would you fix the upload path?
- When a seller uploads a product photo, you need four sizes of thumbnails. Should you generate them inside the upload request or asynchronously? Explain your choice.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.