← Back to portfolio
Case Study

RabbitHole

An event-driven video-streaming platform on AWS — upload a video, an autoscaling fleet of workers transcodes it into adaptive-bitrate HLS, and you stream it back through a CDN, with live status pushed the whole way.

Built as a portfolio piece to demonstrate cloud architecture — event-driven design, a serverless + container hybrid, autoscaling-to-zero, real-time, and cost-awareness — all defined in Terraform. Live demo: rabbithole.stephsimmons.dev ↗ · Code: GitHub ↗

The problem

A streaming service is a textbook asynchronous workload: uploads are fast, but transcoding is slow and bursty. That mismatch is exactly what event-driven, autoscaling infrastructure exists to solve — so RabbitHole is built to demonstrate that architecture rather than fake it with a CRUD app. The goal: take a raw upload to adaptive playback through a fully decoupled pipeline that costs nothing when no one is using it.

What it does

Architecture

An event-driven pipeline that decouples the fast path (upload) from the slow path (transcode), with a serverless API and a container worker fleet — the right tool for each job.

Upload → transcode → stream

BrowserReact · hls.js
1 · presigned URL
API Gateway → LambdaFastAPI · Mangum
2 · PUT file
S3 — uploadsraw video
3 · ObjectCreated event
EventBridge → SQS+ DLQ · retries
4 · poll · autoscale 0→N
ECS Fargate workersffmpeg · HLS renditions
HLS renditions
S3 — streamingprivate · OAC
CloudFrontadaptive playback → UI
Real-time path  DynamoDB (videos) → Stream → Broadcaster Lambda → API Gateway WebSocket → live status in the UI

Highlights

Engineering decisions & trade-offs

Lambda API + Fargate workers

Right tool per job: serverless for the lightweight, bursty API; containers for the long-running, CPU-heavy ffmpeg transcode that would never fit Lambda's runtime and size limits.

Direct-to-S3 upload (presigned)

The API issues a presigned URL and the browser uploads straight to S3, so the API never proxies file bytes — cheaper, faster, and far friendlier to a Lambda execution model.

EventBridge → SQS → workers

Decoupling the pipeline through a queue makes it resilient and fan-out-ready: a DLQ and retries absorb worker failures, and the upload path doesn't block on transcode capacity.

Autoscale on queue depth, down to zero

Step scaling on SQS depth can scale workers up from zero, so there's no compute cost when idle — the single biggest lever for a bursty workload's bill.

No NAT gateway (a documented cost trade-off)

Workers run in public subnets with a zero-ingress security group instead of private subnets behind a NAT gateway. That keeps idle cost near zero for a demo; the production trade-off is noted below.

What I'd change at scale

The demo deliberately optimizes for cost and clarity. Honest production trade-offs:

Stack at a glance

LayerTech
FrontendReact + TypeScript (Vite), hls.js → S3 + CloudFront
APIFastAPI on Lambda (Mangum) + API Gateway
WorkersECS Fargate + ffmpeg, step-autoscaling on SQS depth (min 0)
Real-timeDynamoDB Streams → Lambda → API Gateway WebSocket
MessagingSQS + DLQ, EventBridge, S3 notifications
DataS3 (uploads + streaming), DynamoDB
CDNCloudFront (Origin Access Control)
IaCTerraform
CI/CDGitHub Actions
ObservabilityCloudWatch metrics / alarms, structured logs
My role — Sole architect and engineer: the event-driven AWS architecture, the FastAPI/Lambda API, the Fargate ffmpeg worker, the real-time WebSocket layer, the React + hls.js frontend, and the Terraform that provisions all of it.

View live demo ↗  ·  View code ↗  ·  Get in touch ↗