Skip to content
Level 06

Ember — Serverless Image Optimizer

Smaller images. Shorter memory.

A serverless image-optimization pipeline running entirely on AWS, under its own brand — Ember. Drop an image and get WebP/AVIF variants back in seconds, generated by infrastructure you can watch work. The file goes straight from the browser to S3 via a presigned URL, an event-driven Lambda runs sharp to produce three sizes in two formats, and the results are served back through the same CloudFront distribution the app itself runs on. Every resource is defined in AWS CDK.

The problem

Optimizing images for the web is a routine task, but most online tools solve it by uploading your file to an opaque server that gives no account of what happens to it next. Ember answers a narrower question: what if the whole thing were verifiably serverless — direct-to-storage upload, event-driven processing, and automatic deletion — with every piece of it public as code?

Built with

AWS CDKLambdaS3CloudFrontsharp

What went into this

  • Direct-to-S3 uploads via presigned URLs — the file never touches a server we run, straight from the browser to the bucket
  • Event-driven Lambda pipeline — an S3 ObjectCreated trigger runs sharp to generate WebP and AVIF variants at three sizes, never upscaling past the source
  • Complete infrastructure as code in AWS CDK — every bucket, Lambda, IAM policy, and the CloudFront distribution itself, torn down cleanly with a single cdk destroy
  • A real ~60-minute autodestruction engineered around a genuine platform limit — S3 Lifecycle can only expire objects at day granularity, so an EventBridge-scheduled sweeper does the actual deletion instead
  • A ground-up brand built for this one tool — name, palette, type, logo, and voice — with an on-page example gallery showing all six real generated variants, downloadable, from an actual run of the live pipeline

How it fits together

A single CloudFront distribution fronts three completely different origins — a static frontend, a Lambda Function URL, and an S3 bucket of generated variants — so the app is same-origin end to end and the browser never has to reason about CORS beyond the one unavoidable direct-to-S3 upload. The interesting constraint wasn't the happy path; it was working within what the platform actually allows: CloudFront caches errors by default, so the manifest the frontend polls has to actively defeat that; a Function URL behind CloudFront reports the edge's IP, not the visitor's, so rate limiting needed the forwarded viewer address instead; and S3 Lifecycle simply cannot expire an object in under a day, so the real autodestruction is a small scheduled Lambda, not a bucket setting.

Ember serverless media pipeline — architecture diagramEDGEBrowserdrag & drop UIEDGECloudFront/* → site/api/* → presign/r/* → outputsCOMPUTEPresign Lambdavalidate · rate limit · signFunction URL, IAM authDATADynamoDBper-IP rate limit, TTLSTORAGES3 · uploadsdirect browser PUT1-day lifecycle backstopCOMPUTEProcess Lambdasharp · WebP + AVIF3 sizes × 2 formatsS3 ObjectCreated triggerSTORAGES3 · outputsvariants + manifest.json1-day lifecycle backstopSCHEDULEEventBridge Rulerate(10 minutes)COMPUTECleanup Lambdasweeps objects > 60 min old① GET /api/presign② presigned PUT(direct to S3)③ S3 event④ write variants⑤ poll GET /r/{jobId}/manifest.jsondeletes stale objects in both buckets — the real~60-min mechanism; S3 Lifecycle can't express"1 hour" (day granularity only)
  1. The browser asks CloudFront's /api/* route for a presigned upload URL from the Presign Lambda, which validates the request and rate-limits by IP via DynamoDB.
  2. The browser uploads the file directly to the S3 uploads bucket with that presigned URL — the file never touches a server.
  3. The S3 upload fires an ObjectCreated event that triggers the Process Lambda.
  4. The Process Lambda runs sharp to generate WebP and AVIF variants at three sizes, then writes them plus a manifest.json to the S3 outputs bucket.
  5. The browser polls the manifest through CloudFront's /r/* route until it's ready, then the results are served from the same distribution.
  6. An EventBridge rule runs every 10 minutes and triggers a Cleanup Lambda that sweeps objects older than 60 minutes from both buckets — the real autodestruction mechanism, since S3 Lifecycle can only expire objects at day granularity, not by the hour.
How it works