System design interview

Design an image hosting service

Direct uploads, resized variants behind a CDN, EXIF stripping and deletes.

At a glance

Level
Foundation
Topics
Files and media, Caching
Main parts
CDN · Image API · Image origin · Object storage · Metadata DB · Image worker · Upload events
What you'll learn
Practise this problem
Overview
CDNClientImage APIImage originObjectstorageMetadata DBImage workerUploadeventsCDNcaches variantsClientImage APIsigns, checksImage originObject storageoriginals, sizesMetadata DBImage workerchecks, resizesUpload events

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. The client says it wants to upload a 4 MB JPEG. The API checks the user's quota and the declared type and size.
  2. The API records a pending image with a new ID and a random storage key. It never uses the user's file name.
  3. It returns a signed URL, valid for ten minutes, that allows one upload to that key up to 20 MB.
  4. The client uploads the file straight to object storage using that URL.
  5. Storage publishes an event that an object was created. The queue holds it until a worker is free.
  6. An image worker takes the event.
  7. It reads the file, decodes it, strips metadata, makes the standard sizes and writes them back to storage.
  8. It records the content hash and the variants, and marks the image ready. Only now is the link served.
  • CDN: caches variants
  • Client
  • Image API: signs, checks
  • Image origin: id to blob, resizes
  • Object storage: originals, sizes
  • Metadata DB: owner, hash, status
  • Image worker: checks, resizes
  • Upload events
On this page

In 2 minutes

Key ideas
  • The numbers. These are heavy bytes, not heavy requests. Keep the bytes off our own servers: uploads go straight to storage and views are served from a CDN.
  • Upload path. Direct to object storage with a presigned URL.
  • Image sizes. Common sizes ahead of time, the rest on demand from an allowlist.
  • What we keep as the original. A re-encoded copy without metadata.
  • Links. Immutable links cached for a year.
Read the 2-minute answer

The workload is heavy bytes, not heavy requests, so I keep the bytes off our servers. A client asks an image API for an upload slot. The API checks quota, records a pending image in a metadata database, and returns a presigned URL for one upload of up to 20 MB to a random key. The client sends the file straight to object storage, which emits an event to a queue.

An image worker checks magic bytes, decodes the file, limits pixel count, strips EXIF while applying rotation first, hashes the content to dedupe, makes three standard sizes and marks the image ready. Viewers get images from a CDN, with links that never change, so they cache for a year. On a miss, a thin image origin checks the metadata DB, returns 404 for deleted or pending images, finds the blob and reads or makes the variant. At 500 million views a day and a 95% hit ratio, the origin sees about 290 requests a second.

Rare sizes are made on demand, but only from an allowlist, with a strict cache key, so nobody can fill the cache. A delete marks the record, so the origin answers 404, purges the exact variant keys, deletes the image's own variants and removes the original when no record uses it. The trade-off is a two-step upload and a purge for every delete.

Step 1 of 6: Clarify the scope

Questions to ask first

Ask these before you draw anything. Guess why each answer changes the design, then open it to check.

What is the largest file, and which formats do we accept?
Why it matters: The limit and the format list decide how uploads travel, and what the checks must reject.
Is there a fixed list of sizes, or can a page ask for any size?
Why it matters: A fixed list can be made ahead of time. Any size means resizing on demand, and then abuse becomes a risk.
Are images public, or do some need access control?
Why it matters: Public images can sit in a shared cache for a year. Private ones need signed links that expire.
Where are the viewers, and how many views per upload?
Why it matters: A wide audience and a heavy read ratio mean a CDN does most of the work, not our own servers.
When someone deletes an image, how fast must it disappear?
Why it matters: A cached copy outlives the delete unless we purge it, so the delete path touches the cache as well as storage.

Requirements

Functional

  • Upload an image of up to 20 MB in JPEG, PNG or WebP format.
  • Serve each image at a small set of sizes through a stable link.
  • Let the owner delete an image, and stop serving it everywhere.
  • Remove private metadata, such as GPS location, from every image we serve.
  • Store identical files only once.

Non-functional

  • A cached image arrives with p99 under 100 ms.
  • At least 95% of views are answered by the CDN without touching our origin.
  • No accepted upload is ever lost: every original is kept in at least 3 copies.
  • A deleted image stops being served within 5 minutes.

Step 2 of 6: Size it

Show the maths

These numbers are assumptions for the exercise, not facts about a real system:

  • 5 million images are uploaded a day, averaging 3 MB each.
  • There are 500 million image views a day, and the average view sends 100 KB.
  • Three standard variants are made for each image: a thumbnail of 15 KB, a medium size of 100 KB and a large size of 400 KB.
  • The CDN answers 95% of views from its cache.

The arithmetic:

Uploads per second
5M a day ÷ 86,400 s ≈ 58
≈ 58/s on average, about 290/s at a 5x peak
Originals per day
5M × 3 MB = 15,000,000 MB
15 TB a day, about 5.5 PB a year (15 TB × 365)
Variants per day
(15 + 100 + 400) KB = 515 KB per image × 5M
≈ 2.6 TB a day, about 17% of the originals
Views per second
500M a day ÷ 86,400 s ≈ 5,787
≈ 5,800/s on average
Requests reaching the origin
5% misses × 5,800/s
≈ 290/s, easy for a small fleet
Egress bandwidth
500M × 100 KB = 50 TB a day; × 8 bits ÷ 86,400 s
≈ 4.6 Gbit/s on average, mostly carried by the CDN

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

The Client first asks the Image API for an upload slot. The API checks the user's quota, records a pending image in the Metadata DB and returns a short-lived signed URL. The client then sends the file straight to Object storage, so 15 TB a day of image bytes never passes through our API servers.

When the object lands, storage emits an event to the Upload events queue. An Image worker picks it up. It checks that the file really is an image, strips private metadata, hashes the content, makes the standard sizes, writes them back to storage and marks the image ready in the metadata DB. Until then the image is not served.

Viewers load images from the CDN, which asks the Image origin only on a miss. Each link names an image and a size, and never changes, so the CDN can keep it for a year. The origin is a thin service. It looks the image up in the metadata DB, answers 404 for a deleted or pending image, finds the blob key, reads the variant from object storage, and makes an allowlisted size on demand if it does not exist yet. A delete goes through the API, which marks the image deleted and purges the CDN.

Other paths through the system

View an image

  1. A page asks for the medium size of image abc123 at its stable link.
  2. On a miss, the CDN asks the image origin for that one variant.
  3. The origin looks the image ID up in the metadata DB, to get its status and the key of its blob.
  4. The DB says it is ready and names the blob. A deleted or pending image gets a 404 here, and nothing is read.
  5. The origin reads the variant. If it is an allowlisted size not made yet, it resizes the original and stores the result.
  6. The origin returns the bytes. The CDN keeps a copy for a year because the link never changes.
  7. The client receives the image. Later viewers in the same region get it straight from the CDN.

Delete an image

  1. The owner asks to delete an image.
  2. The API marks the record deleted, so the origin answers 404 for it. A job deletes its variant files and, once no record shares it, the original.
  3. The API asks the CDN to purge the image's variants, whose keys are listed in the record, so cached copies stop being served.

Step 4 of 6: Go deep on the hard part

Direct uploads and file checks

InterviewerHow do uploads reach storage without loading your servers, and how do you stay safe from bad files?Sending 15 TB a day through API servers would tie up connections and bandwidth for no benefit.

Sending 15 TB a day through API servers would tie up connections and bandwidth for no benefit. Instead the client uploads directly to object storage with a presigned URL: a link the API signs, which allows exactly one action and expires soon.

The API fills the signature with rules. It names the exact storage key, which is a random ID and never the user's file name. It sets a time limit of about ten minutes. It caps the size, since object stores can enforce a content-length range in the signed policy. It also fixes the content type. Large files use multipart upload, so a dropped connection resumes from the last part. A rule on the bucket deletes pending objects that were never confirmed after a day.

Do not trust any of that for safety. The declared type and the file extension are claims from the client. The worker checks the real thing:

  • Magic bytes. A JPEG begins with FF D8 FF, and a PNG with 89 50 4E 47. If the bytes do not match an allowed format, reject the file.
  • A full decode. Open it with a well-maintained image library. A file that does not decode is not an image.
  • A pixel limit. A small file can describe a huge picture. A PNG of 30,000 by 30,000 pixels is 900 million pixels, which needs about 3.6 GB of memory at 4 bytes a pixel. Read the dimensions from the header first and refuse anything above, say, 50 megapixels, which is about 200 MB decoded.
  • A re-encode. Never serve the uploaded bytes as they are. Writing new pixels into a fresh file removes anything hidden inside, such as a script placed after the image data.

Serve images from a separate, cookie-free domain with a correct Content-Type and the header X-Content-Type-Options: nosniff, so a browser never treats an image as a page. Rate limit uploads per user, and keep the state pending until every check passes.

Variants: ahead of time or on demand

InterviewerShould you make every size at upload time, or resize when someone asks? How do you stop abuse?There are two ways to get a 640-pixel-wide copy.

There are two ways to get a 640-pixel-wide copy.

  • Ahead of time. The worker makes a fixed set, say 150, 640 and 1,600 pixels wide, when the image arrives. CPU use is steady and predictable, and the first viewer is never slow. The cost is wasted storage on sizes nobody opens, and a back-fill job whenever the set changes.
  • On demand. The link carries the size, such as /abc123/w640.webp. On a CDN miss, the image origin makes it, returns it and the CDN keeps it. Nothing is wasted and any size is possible. The first viewer waits maybe 100 to 300 ms. The danger is abuse: an attacker can request widths 1, 2, 3 and up to 100,000, each a CDN miss and a CPU-heavy resize, and fill the cache with junk.

The defence for on-demand is a variant allowlist. Accept only sizes from a short list, such as 150, 320, 640, 1,024 and 1,600 pixels, and only the formats we choose, WebP and JPEG. Anything else gets a 400 before any work is done. That caps a single image at 5 sizes × 2 formats = 10 cached objects, instead of an unlimited number.

Next, control the cache key. It should be the image ID plus the variant name plus the format, and nothing else. Ignore unknown query parameters, or an attacker can add ?x=1, ?x=2 and so on to get a fresh miss each time. Collapse concurrent requests, so a thousand viewers who miss at once cause one resize, not a thousand. Cap the number of resizes running at once.

My pick is a hybrid. Make the three common sizes ahead of time, so almost every view is a plain storage read. Serve the rest of the allowlist on demand, by the image origin. Put a version or content hash in the link, so a link is immutable and can be cached for a year with the immutable directive.

Dedupe, metadata and deletes

InterviewerHow do you avoid storing the same file twice, protect people's privacy, and make a delete real?Dedupe. The worker computes a SHA-256 hash of the cleaned original.

Dedupe. The worker computes a SHA-256 hash of the cleaned original. If a blob with that hash already exists, it does not store a second copy. Each image record points to a shared blob of the original, and a reference count tracks how many records use it. The variants are not shared. Each record owns its own, made from the shared original, so deleting one image never touches another's sizes. The stable link names the image ID, so the origin must look the ID up in the metadata DB to find the blob. The bucket alone cannot answer that. Popular memes upload thousands of times, so this saves real space. It catches only exact copies. A resized or re-compressed version has a different hash. Do the check on the server only. If an upload could finish instantly when the file already exists, an observer could learn that someone else holds that file.

Metadata. Photos from a phone often carry an EXIF block with GPS coordinates, the camera's serial number and timestamps. Sharing a photo with an exact home location is a real harm. Strip all of it from everything we serve. One catch: the orientation tag. If you drop it without care, portrait photos turn sideways. So apply the rotation to the pixels first, then remove the tag. Convert colour profiles to sRGB so images look the same in every browser.

Deletes. A delete has more layers than it seems, so do them in order:

  • Mark the record deleted in the metadata DB. This is the source of truth. Every CDN miss goes through the image origin, which reads the record and answers 404 for a deleted image, so a purged copy cannot come back.
  • Purge the CDN. The record lists every variant key, at most about ten, so the purge is exact. Purges are not instant: allow seconds to a few minutes, which fits the 5-minute target.
  • Delete the image's own variant objects at once, since only the original is shared. Lower the original blob's reference count. At zero, a cleanup job deletes it from storage.
  • Backups age out at the end of their retention period.

Links are immutable and cached for a year, so a purge is the only way to remove a cached copy from the CDN. Be honest about the limit: a browser that already holds the image, or a person who saved it, keeps it. Say that plainly in the privacy policy.

Step 5 of 6: Name the trade-offs

Upload path
Chose
Direct to object storage with a presigned URL
Over
Through our API servers

Image bytes never load our servers, and uploads scale with the storage service. The cost is a two-step flow and an event-driven worker to process the file afterwards.

Image sizes
Chose
Common sizes ahead of time, the rest on demand from an allowlist
Over
Every possible size on demand, or every size ahead of time

Most views are a plain read and rare sizes cost nothing until used. The cost is two code paths and care with the allowlist.

What we keep as the original
Chose
A re-encoded copy without metadata
Over
The raw upload, untouched

Nothing private or hidden can ever leak out. The cost is that we cannot give back the exact original bytes.

Links
Chose
Immutable links cached for a year
Over
Mutable links with a short cache time

The CDN hit ratio stays very high and the origin stays small. The cost is that deletes and changes need a purge, and a new link for any edit.

Step 6 of 6: Say what breaks next

Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.

What happens when one image goes viral?
The CDN absorbs it. Because the link is immutable, nearly every request is a hit, and the origin sees about one request per CDN location. An origin shield, a middle cache layer that all locations ask first, reduces that to one request in total.
How would you serve private images?
Use signed links that carry an expiry time and a signature over the path. The CDN checks the signature at the edge, so an invalid link never reaches the origin. Keep expiry short for sensitive images, because a leaked link works until it expires.
How do you handle animated GIFs?
A GIF is large and slow for what it shows. Convert it to a short looping video in a modern format and serve that, which is often a fraction of the size. Keep the still first frame as the thumbnail.
How do you keep unsafe content out?
Add checks in the worker before an image is marked ready, such as matching its hash against lists of known bad files and running a classifier. Give users a way to report images, and a review queue for borderline cases. Because every image starts pending, nothing is public until it passes.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.