4. Uploading media

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 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:

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:

Cache key: media URL. TTL: long (assets are immutable — a new version means a new media_id). Invalidation: rarely needed.