Version 1 — The naive approach
Users upload an image → your server receives it → you save it to the server’s local disk → record the file path in the DB → return a URL. This works for prototypes and hackathons, but quickly breaks at scale.
- Disk limits: a server with limited disk (e.g., 50 GB) will fill quickly if users upload tens of GBs per day.
- Single point of failure: if the server dies, images may be lost unless you implement replication/backups.
- Operational overhead: scaling, backups, and restorations become painful.
Version 2 — Move to object storage
Replace server disk with object storage (S3, Google Cloud Storage, Azure Blob Storage). The app server stores objects into S3 and stores object keys/URLs in the DB. This provides practically unlimited storage, built-in redundancy, and resilience.
- Use presigned upload URLs to allow clients to upload directly to object storage, reducing server bandwidth and cost.
- For very large files, use multipart uploads (S3 Multipart Upload, GCS resumable uploads).
- Record metadata in your DB: object key, content-type, owner ID, upload timestamp, checksum.
Use presigned (signed) upload URLs so clients upload directly to object storage. This reduces server bandwidth and simplifies horizontal scaling.
New problem: raw camera files are large
Mobile camera images can be 10–20+ MB. Serving raw camera files to mobile clients causes slow page loads and poor UX. You need to optimize images for different devices and network conditions.Version 3 — Asynchronous processing pipeline
Do not block the upload HTTP request on heavy processing. Instead, make optimization asynchronous:- Client uploads the raw image to object storage (ideally via a presigned URL).
- Your API server performs lightweight validation, records metadata in the DB, and enqueues a “process this object key” message to a durable queue (SQS, Pub/Sub, RabbitMQ, Kafka).
- Workers/consumers pick up jobs, then:
- Generate multiple sizes (thumbnail, small, medium, large).
- Convert/optimize formats (WebP, AVIF) for bandwidth savings.
- Strip sensitive EXIF metadata.
- Calculate and persist checksums and content hashes.
- Write processed derivatives back to object storage and update the DB.
- Mark processing status in the DB (e.g.,
processing,ready,failed) so clients can show conditional UI immediately after upload.

- Idempotency: design workers so retries do not create duplicate derivatives or corrupt state. Use deterministic object keys or a deduplication step.
- Durable queues + retries: use exponential backoff and dead-letter queues for poison messages.
- Status tracking: persist job states and errors so the API can report progress to clients.
- Deduplication: compare checksums/hashes to avoid reprocessing identical uploads.
Make processing idempotent and persist job status. That simplifies retries, debugging, and recovery from partial failures.
Version 4 — Add a CDN for global performance
Even optimized images benefit from being cached geographically close to users. Put a CDN (CloudFront, Fastly, Cloudflare, etc.) in front of your object storage.- First request (cache miss): edge fetches the object from origin (S3).
- Subsequent requests: served from the nearest edge node with low latency.
- CDNs handle the majority of read traffic, enabling virtually unlimited, low-latency reads.
- Use
Cache-Controlheaders to set TTLs and leverage CDN caching effectively. - Use versioned/fingerprinted URLs (e.g.,
image-<hash>.webp) so updates don’t require manual invalidation — clients request a new URL when content changes. - For private assets, use signed URLs or signed cookies at the CDN layer to enforce access controls.

- Use immutable URLs with content hashes:
image-<hash>.webp. - Keep short cache TTLs for frequently changing content, or use versioning to avoid TTL complications.
- For signed/private images, prefer CDN-level signed URLs over exposing direct S3 presigned URLs to end users.
Version 5 — Validation, safety, and moderation
Before images enter processing or become public, validate and protect:- Verify file headers (magic bytes) and MIME types — don’t rely on file extensions alone.
- Enforce maximum file size (e.g., reject files > 20 MB).
- Scan for malware and viruses on the raw upload.
- Run automated content moderation (nudity, violence) and implement a human review workflow for flagged content.
- Quarantine suspicious files until cleared and ensure no public access before approval.
- A file named
image.jpgthat is actually a malicious archive or a zip bomb. - EXIF or metadata with payloads that could exploit downstream parsers.
Final architecture — end-to-end summary
- Client uploads raw image (prefer presigned URL to object storage).
- API server performs lightweight validation, stores metadata in DB, and enqueues a processing job.
- Background workers optimize images (resize, reformat, strip metadata), persist derivatives to object storage, and update DB state.
- CDN sits in front of object storage for global caching and low-latency delivery.
- Security pipeline (malware scans, moderation, size/type validation) ensures safety prior to publishing.
References and further reading
- AWS S3: https://aws.amazon.com/s3/
- AWS CloudFront: https://aws.amazon.com/cloudfront/
- Multipart uploads & presigned URLs: consult your cloud provider SDK docs (S3 multipart, GCS resumable)
- Image processing libraries: libvips, ImageMagick, Sharp (Node)
- Content moderation services: Perspective API, AWS Rekognition, Google Cloud Vision