A 280-byte tweet referencing a 5 MB video is fine. The 5 MB video stored in the DB row is a disaster — your buffer cache fills with one user’s vacation footage and tweet read latency collapses. Media gets its own path.
Architecture
Posting media
flowchart TD
Client(["Client"])
GW["API Gateway
(AWS API Gateway)"]
MS["Media Service
(Fargate)"]
TS["Tweet Service
(Fargate)"]
S3[("S3
(blob storage)")]
DB[("DynamoDB
— tweets —")]
Worker["Media Worker
(Fargate)
thumbnails · transcode · moderation"]
Client -->|"① init / ③ post tweet"| GW
GW --> MS
GW --> TS
MS -->|"presigned URL + media_id"| Client
Client -->|"② PUT bytes"| S3
TS -->|"media_ids ref only"| DB
MS ~~~ S3
TS ~~~ DB
S3 -->|"S3 event (async)"| Worker
Worker -->|"processed assets"| S3
Reading media
flowchart TD
Client(["Client"])
CDN["CDN
(CloudFront / Fastly)"]
S3[("S3
(blob storage)")]
Client -->|"GET asset"| CDN
CDN <-->|"origin (cache miss)"| S3
The upload flow
1. Client → POST /v1/media/init → server returns presigned S3 PUT URL + media_id
2. Client → PUT https://s3.../... → uploads bytes directly to S3
3. Client → POST /v1/tweets {media_ids:[...]}
Three things this buys you:
- The application server never sees the bytes. Upload bandwidth bypasses your origin entirely.
- Retries are cheap. If the upload fails on a flaky network, the client retries just step 2, not the whole post.
- S3 is your durability story (eleven nines). You don’t replicate JPEGs yourself.
The tweet POST in chapter 3 only carries media_ids. The tweets table stores those IDs as a list attribute — never the bytes.
Async processing
After the PUT lands, an S3 event triggers a worker that:
- Generates thumbnails (mobile, desktop, retina).
- Transcodes video to multiple bitrates (HLS).
- Runs content moderation (NSFW, CSAM hash matching).
- Writes the processed asset back to S3 under a new key.
This is async. The tweet can post before processing completes; the client shows a “processing…” state until it’s done. The user experience never blocks on transcoding a 4K video.
The worker runs on Fargate rather than Lambda. Image thumbnailing fits Lambda comfortably, but a long video transcode can run past the 15-minute cap, and HLS ladder generation can blow past the 10 GB memory limit. Putting just video on Fargate and the rest on Lambda would mean two pipelines to operate; Fargate handles every media type with one.
Serving via CDN
Origin S3 is too slow and too expensive to serve every read. Put a CDN (CloudFront, Fastly) in front:
- Edge nodes cache assets close to users — much lower latency than hitting origin across regions.
- Origin requests drop dramatically on hot content.
- Bandwidth costs drop because CDN egress is far cheaper than S3 egress at scale.
Cache key: media URL. TTL: long (assets are immutable — a new version means a new media_id). Invalidation: rarely needed.