- A raw upload is not what users download. The media pipeline turns one original into many renditions: validated, privacy-scrubbed (EXIF/GPS removed), correctly rotated, resized, re-encoded (JPEG/WebP/AVIF), plus thumbnails and tiny blurred placeholders.
- Videos are transcoded into a bitrate ladder (240p…1080p) with modern codecs (H.264, HEVC, VP9, AV1), cut into 2–6 s HLS/DASH segments, so players can do adaptive bitrate streaming.
- Work is driven by Kafka events into autoscaled worker pools that execute a task DAG with retries, idempotent outputs, priorities and (for video) GPU/ASIC hardware encoders.
- Content moderation runs in the pipeline: perceptual-hash matching (PhotoDNA, PDQ) against known illegal content, ML classifiers, and human review queues; ML also generates automatic alt text.
- A post state machine (UPLOADED → PROCESSING → PUBLISHED, or FAILED/REJECTED) keeps it all consistent; lazy transcoding of rare renditions and spending expensive encodes only on popular videos keep costs sane.
1. The problem: why process media at all?
A modern phone produces a 12–48 megapixel photo of 2–6 MB (often in Apple's HEIC format) and 4K HDR video at 20–50 Mbit/s. Now think about who views it on Instagram: a user on a 5-year-old Android phone with a 720-pixel-wide screen on a congested 3G network in a rural area, a user on a new iPhone on Wi-Fi, and someone on a desktop browser that can't decode HEIC at all. Serving the original to all of them would be wrong in every way:
- Too big: downloading 5 MB to display a 640-pixel-wide image wastes ~95% of the bytes, the user's data plan and our CDN bandwidth. Feeds would scroll at a crawl.
- Incompatible: not every device decodes HEIC, HEVC, AV1 or HDR.
- Unsafe for privacy: photos carry EXIF metadata including the exact GPS coordinates where they were taken — often the user's home.
- Unsafe for the platform: uploads may be malformed (or deliberately malicious) files, or prohibited content that must never be published.
- Not streamable: a single large video file can't adapt to a network that drops from 20 Mbit/s to 1 Mbit/s mid-playback.
So every upload goes through a media processing pipeline that produces a set of safe, optimised, device-appropriate renditions, stores them in object storage, and lets the CDN serve the right one to each viewer.
2. The pipeline at a glance
The order matters: validate before decoding further (security), normalise before producing renditions (so every rendition is upright and metadata-free), run renditions and moderation in parallel, and only flip the post to PUBLISHED when the required outputs and checks are complete.
3. Image processing stages
3.1 Validation
- Check the real type by magic bytes (
FF D8 FF= JPEG,89 50 4E 47= PNG,ftypheic= HEIC), never the file extension or the client-suppliedContent-Type. - Limits: max file size (e.g. 50 MB photo), max dimensions and total pixel count. A tiny 50 KB PNG can declare 50,000 × 50,000 pixels and expand to 10 GB in memory — a decompression bomb. Read the header first and reject before decoding.
- Decode in a sandbox: image/video decoders are complex C code with a long history of vulnerabilities (e.g. the 2016 "ImageTragick" ImageMagick flaw allowed remote code execution via crafted images). Run workers with least privilege, seccomp/containers, no network access except storage, and a strict allow-list of formats.
- Integrity: verify the uploaded size/checksum matches what the client declared; compute a content hash (for dedup and for moderation hash checks).
- Video: probe with
ffprobe: container, codecs, duration (Reels max length), resolution, frame rate, rotation, audio presence, corrupted streams.
3.2 EXIF stripping (privacy)
EXIF (Exchangeable Image File Format) metadata is embedded by cameras: camera make/model, lens, exposure, timestamps, sometimes the device serial number, a thumbnail of the original, and — critically — GPS latitude/longitude, accurate to a few metres. Publishing it would reveal where users live, where their children go to school, or where a vulnerable person is hiding.
$ exiftool IMG_4821.HEIC
Make : Apple
Model : iPhone 15 Pro
Date/Time Original : 2026:10:23 14:41:07
Orientation : Rotate 90 CW
GPS Latitude : 19 deg 4' 33.12" N ← user's home, to ~3 m
GPS Longitude : 72 deg 52' 40.56" E
Lens Model : iPhone 15 Pro back triple camera 6.765mm f/1.78
The pipeline re-encodes every rendition from decoded pixels and writes no metadata except what's needed for correct display (e.g. an ICC colour profile). Anything useful (capture time, a location the user chose to tag) is stored as structured fields in the post's metadata DB, under the user's control — not hidden in the file. Re-encoding (rather than editing tags) also defeats metadata hidden in unusual places (XMP, IPTC, maker notes, trailing bytes after the image).
3.3 Orientation fix
Phone sensors capture in one fixed orientation. Instead of rotating pixels, the camera saves them as captured and sets the EXIF Orientation tag (values 1–8: normal, mirrored, rotated 90°/180°/270°, and mirrored+rotated). Viewers are supposed to apply it. If you strip EXIF without first applying the rotation, half your portrait photos show up sideways. So: read the Orientation tag, physically rotate/flip the pixels, then drop the tag. (Videos have a similar rotation flag in the container metadata.)
3.4 Resizing to multiple renditions
Generate a fixed set of widths covering common screen sizes and layouts — for example 1080, 750, 640, 480 and 320 px for the feed, plus small square crops (150–240 px) for profile grids and thumbnails. Instagram displays feed photos between 1.91:1 landscape and 4:5 portrait, 1080 px wide at most, so larger inputs are scaled down and out-of-range aspect ratios are cropped.
- Resampling filter: high-quality downscaling uses filters such as Lanczos or Catmull-Rom; naive nearest-neighbour causes aliasing (jagged edges, moiré). Add mild sharpening after large reductions.
- Resize from the best source once, or cascade (1080 → 640 → 320) for speed — fewer decode passes, slight quality cost.
- Colour: convert wide-gamut (Display P3) and HDR inputs to sRGB/SDR for broad compatibility, or keep a P3-tagged variant for capable devices.
- Fast libraries: libvips (streaming, low memory) is typically several times faster than ImageMagick for this workload; JPEG decoders can also decode at 1/2, 1/4 or 1/8 scale directly ("shrink-on-load"), a huge saving when making thumbnails.
3.5 Format conversion: JPEG, WebP, AVIF
| Format | Typical size vs JPEG at similar quality | Encode speed | Support | Notes |
|---|---|---|---|---|
| JPEG (1992) | 1× (baseline) | Very fast | Universal | Use an optimising encoder (mozjpeg) and progressive mode so a blurry full image appears early. |
| WebP (2010) | ~25–35% smaller | Fast | All modern browsers, Android, iOS 14+ | Supports alpha and animation; good default for app and web. |
| AVIF (2019) | ~40–50% smaller | Slow (5–20× JPEG) | Modern browsers, Android 12+, iOS 16+ | Based on the AV1 video codec; supports HDR/wide gamut. Best bytes, most CPU. |
| HEIC/HEIF | ~40–50% smaller | Medium | Apple devices; limited elsewhere | Common input format from iPhones; patent licensing limits use as an output. |
| JPEG XL | ~35–60% smaller; lossless JPEG recompression ~20% | Medium | Limited (Safari; others partial) | Promising, not yet universal. |
The API returns a set of candidate URLs (or the app requests by capability), and the client picks the best format it can decode and the smallest width ≥ its display width × pixel density. See Clients.
3.6 Quality vs compression trade-offs
Lossy encoders expose a quality knob (JPEG/WebP "q" 0–100, AVIF quantizer/CRF). Higher quality → bigger files; beyond ~85 the extra bytes are mostly invisible. Practical techniques:
- Pick quality per rendition: thumbnails tolerate lower quality (q≈70) than a full-screen 1080 px image (q≈80–85).
- Chroma subsampling 4:2:0: store colour at half resolution (eyes are less sensitive to colour detail than brightness) — ~30–50% savings for little visible loss.
- Perceptual targets instead of fixed q: measure quality with metrics such as SSIM, Butteraugli or VMAF and binary-search the lowest quality that meets a target score. A flat blue sky compresses far better than a forest, so per-image tuning can save another ~10–30%.
Illustrative sizes for one 1080×1350 photo (varies a lot with content): original HEIC from phone ............. 3.2 MB (4032×5040) JPEG q=85 progressive (mozjpeg) ...... 260 KB WebP q=80 ............................ 180 KB (-31%) AVIF cq=28 ........................... 120 KB (-54%) 640 px WebP ........................... 70 KB 150 px thumbnail WebP ................. 6 KB BlurHash placeholder .................. ~30 bytes (a text string!)
3.7 Thumbnails and placeholders (blurhash)
Even a 70 KB image takes a moment on a slow network. To avoid blank grey boxes while scrolling, the pipeline produces placeholders small enough to embed directly in the feed API's JSON, so they're shown instantly before any image request:
- Dominant colour — a single hex colour (a few bytes).
- BlurHash — the image is reduced to a handful of low-frequency colour components (a small grid of DCT-like coefficients, e.g. 4×3) and encoded as a ~20–30 character base83 string such as
LEHV6nWB2yk8pyo0adR*.7kCMdnj. The client decodes it into a smooth, blurred gradient matching the photo's colours and layout, then cross-fades to the real image. - Tiny preview (LQIP) — a very low-resolution JPEG/WebP (e.g. ~40 px wide, a few hundred bytes) scaled up with a blur. Facebook publicly described sending ~200-byte preview photos inside API responses by stripping the standard JPEG header (which the app already knows) from tiny thumbnails.
- Video poster frame / animated preview — a representative frame (chosen by a quality/face heuristic, not just frame 0) and short low-res preview clips for grids.
4. Video processing
4.1 Codecs vs containers
Two terms beginners often confuse:
- A codec (coder/decoder) is the compression algorithm for the video (or audio) stream itself: H.264, HEVC, VP9, AV1 for video; AAC or Opus for audio.
- A container is the file format that wraps one or more encoded streams plus timing and metadata: MP4 (and fragmented MP4 / CMAF), MOV, WebM, MPEG-TS. "An .mp4 file" says nothing certain about which codec is inside.
Video codecs exploit redundancy in space (within a frame, like JPEG) and in time (consecutive frames are similar). Frames come in three kinds: I-frames (keyframes — self-contained, large), P-frames (store differences from previous frames using motion vectors) and B-frames (reference both past and future frames — smallest). A GOP (group of pictures) runs from one keyframe to the next. A player can only start decoding — or switch quality — at a keyframe, so keyframes must be placed at every segment boundary, aligned across all renditions (e.g. a keyframe every 2 s with 4 s segments).
| Codec | Year | Bitrate vs H.264 at equal quality | Encode cost | Device support | Licensing |
|---|---|---|---|---|---|
| H.264 / AVC | 2003 | 1× (baseline) | Low; hardware everywhere | Universal — every phone, browser, TV | Patent pools (royalties) |
| H.265 / HEVC | 2013 | ~30–50% less | ~2–10× H.264 in software | Apple devices, most newer Android hardware; patchy in browsers | Complex, multiple patent pools |
| VP9 | 2013 | ~30–40% less | ~5–10× H.264 | Chrome, Firefox, Android; YouTube's workhorse | Royalty-free (Google) |
| AV1 | 2018 | ~30% less than VP9/HEVC, ~50% less than H.264 | High in software (SVT-AV1 made it practical); hardware decode in newer devices | Modern browsers, newer phones with AV1 decoders | Royalty-free (Alliance for Open Media, incl. Meta) |
The usual strategy is H.264 for everyone (guaranteed playback) plus a more efficient codec (VP9/AV1, or HEVC for Apple) for devices that support it — and, because those encodes are expensive, often only for videos that will be watched a lot (section 7).
4.2 The bitrate ladder
A bitrate ladder is the set of resolution/bitrate pairs a video is encoded into. Each "rung" is a complete rendition. Illustrative ladder for a vertical 9:16 Reel (actual values depend on content, codec and encoder):
| Rung | Resolution (9:16) | H.264 video bitrate | AV1 video bitrate (≈) | 30 s clip size (H.264, incl. 64–128 kbps audio) | Typical network |
|---|---|---|---|---|---|
| 240p | 240×426 | ~300 kbps | ~150 kbps | ~1.4 MB | Poor 3G |
| 360p | 360×640 | ~600 kbps | ~300 kbps | ~2.5 MB | 3G |
| 480p | 480×854 | ~1.0 Mbps | ~500 kbps | ~4.0 MB | Weak 4G |
| 720p | 720×1280 | ~2.0 Mbps | ~1.0 Mbps | ~7.9 MB | 4G |
| 1080p | 1080×1920 | ~3.5–5 Mbps | ~2.0–2.5 Mbps | ~14–19 MB | Good 4G / 5G / Wi-Fi |
(Size check for 720p: 2.128 Mbps × 30 s ≈ 64 Mbit ≈ 8 MB.) Per-title / per-scene encoding — popularised publicly by Netflix — adapts the ladder to content: a talking head against a plain wall looks great at half the bitrate of a confetti-filled concert clip, so you measure quality (e.g. VMAF) and choose the cheapest bitrate per rung that hits the target.
# one rung of the ladder with ffmpeg (H.264, 720p, keyframe every 2 s @ 30 fps, 4 s fMP4 segments)
ffmpeg -i input.mov -vf "scale=720:1280" -c:v libx264 -preset slow -crf 23 \
-maxrate 2000k -bufsize 4000k -g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -f hls -hls_time 4 -hls_segment_type fmp4 \
-hls_playlist_type vod 720p/index.m3u8
4.3 Segmenting: HLS and DASH
Each rendition is cut into short segments (typically 2–6 s) plus manifests that describe them:
- HLS (HTTP Live Streaming, Apple):
.m3u8text playlists — a master playlist listing the variants and a media playlist per variant listing segments. - MPEG-DASH: an XML
.mpdmanifest describing "representations" and segment templates. - CMAF: a common fragmented-MP4 segment format both HLS and DASH can reference, so you store segments once and serve two manifests.
#EXTM3U <- master.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=428000,RESOLUTION=240x426,CODECS="avc1.4d4015,mp4a.40.2"
240p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2128000,RESOLUTION=720x1280,CODECS="avc1.4d401f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4128000,RESOLUTION=1080x1920,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
#EXTM3U <- 720p/index.m3u8
#EXT-X-TARGETDURATION:4
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.0,
seg_000.m4s
#EXTINF:4.0,
seg_001.m4s
#EXT-X-ENDLIST
Segments are small immutable files — perfect for CDN caching. Short-form apps also commonly use a single progressive MP4 per rendition fetched with HTTP Range requests for the very first seconds (fastest start), switching to ABR segments as playback continues.
4.4 Adaptive bitrate (ABR) playback
The player downloads the manifest, then fetches segments one at a time, choosing the rung for each segment based on measured throughput and how many seconds of video are buffered:
bandwidth ███████████▇▇▅▃▂▂▂▃▅▇██████ (Wi-Fi → walks into lift → back)
rung chosen 1080 1080 1080 720 480 360 360 480 720 1080 1080
buffer (s) [4][8][12][14][12][9][8][8][10][12][14]
↑ buffer drains while bandwidth is low; player steps DOWN
before the buffer empties, so playback never stalls
- Throughput-based algorithms pick the highest rung below the recent measured bandwidth (with a safety margin).
- Buffer-based algorithms (e.g. BOLA) pick rungs from buffer level: low buffer → low rung, full buffer → climb.
- Fast start: begin with a low/medium rung (and prefetch the first segment of the next Reels in the feed) so playback starts in < 200 ms, then climb.
- The goal is to minimise rebuffering (stalls hurt engagement far more than slightly lower resolution) while maximising quality and avoiding constant switching.
5. Orchestrating the pipeline
5.1 Kafka → worker pools → task DAG
When an upload completes, the Upload Service publishes media.uploaded to Kafka. A pipeline orchestrator consumes it and expands it into a DAG (directed acyclic graph) of tasks, each executed by a specialised worker pool:
- Chunked parallel encoding: a 90 s Reel split into 10 s chunks (at keyframes) × 5 rungs = 45 independent encode tasks; with enough workers the wall-clock time approaches the time to encode a single chunk.
- Separate pools per resource type: CPU pools for images, GPU/ASIC pools for video, GPU pools for ML moderation — each sized and autoscaled independently.
- Workflow state (which tasks are done, outputs, attempts) lives in a database or a workflow engine (Temporal, AWS Step Functions, Netflix Conductor), not in worker memory, so any worker can resume a half-finished DAG.
5.2 Retries and idempotency
Workers crash, spot instances vanish, storage calls time out; Kafka delivers at least once. Every task must therefore be safe to run twice:
- Deterministic output keys: the output location is a pure function of the input, e.g.
{hash}/p/{media_id}/v1/720p/seg_003.m4s. Re-running overwrites the same object with identical content — harmless. - Check before work: if the output already exists (HEAD request) and the task is marked done, skip.
- Conditional state updates: mark tasks done with compare-and-set (
UPDATE … SET state='DONE' WHERE task_id=? AND state='RUNNING'). - Retry policy: exponential backoff with jitter for transient errors; a max attempt count; permanent errors (corrupt file) fail fast; exhausted retries go to a dead-letter queue and the post to
FAILEDwith a user-visible "upload failed, retry?" (see Resilience Patterns). - Timeouts / leases: a task claimed by a worker that dies is re-queued after its lease expires.
5.3 Priorities and autoscaling
- Critical path first: to publish quickly, produce only what's needed for the first view — e.g. the 1080 + 640 px images, a thumbnail and placeholder, or the 360p/720p H.264 rungs for video — in a high-priority queue. The remaining rungs and advanced codecs follow in lower-priority queues after the post is live.
- Separate lanes: interactive uploads vs bulk re-encoding/backfill jobs (e.g. re-encoding old photos to AVIF) never share a queue, so a backfill can't delay a user's post.
- Autoscale on lag / queue age: uploads peak in the evening and around events (New Year); scale pools on consumer lag and oldest-task age rather than CPU alone. Low-priority batch work can soak up idle capacity and cheap spot instances off-peak.
5.4 GPU and hardware encoders
Software encoders (x264, x265, libvpx, SVT-AV1) give the best compression per bit at slow presets but cost lots of CPU. Hardware encoders — GPU blocks such as NVIDIA NVENC and Intel Quick Sync, or dedicated ASICs — encode many streams in real time at a fraction of the power, at somewhat lower compression efficiency than the slowest software presets. Large platforms build their own: Google described its Argos video coding unit for YouTube, and Meta announced the MSVP (Meta Scalable Video Processor) ASIC for transcoding video-on-demand and live streams. A common split: hardware for the fast, high-volume first encodes; high-effort software encodes for the most-watched content where every saved bit is multiplied by millions of views.
6. Content moderation in the pipeline
Instagram must keep illegal and policy-violating content off the platform at the scale of hundreds of millions of uploads per day. Moderation is layered, from cheap and certain to expensive and nuanced:
6.1 Perceptual hashing: known illegal content
A cryptographic hash (SHA-256) changes completely if a single pixel changes, so re-saving, resizing or cropping an image defeats it. A perceptual hash instead summarises what the image looks like: visually similar images get hashes that differ in only a few bits, measured by Hamming distance (the number of differing bits).
# dHash - the simplest perceptual hash, to build intuition (64 bits)
def dhash(img):
g = img.convert("L").resize((9, 8)) # grayscale, 9×8 pixels: throws away detail & colour
px = list(g.getdata())
bits = 0
for row in range(8):
for col in range(8): # is each pixel brighter than its right neighbour?
left, right = px[row * 9 + col], px[row * 9 + col + 1]
bits = (bits << 1) | (left > right)
return bits
def is_match(h1, h2, threshold=10):
return bin(h1 ^ h2).count("1") <= threshold # Hamming distance: few differing bits = same picture
Production hashes are far more robust: PhotoDNA (developed by Microsoft and Dartmouth, licensed to platforms and used industry-wide) and Meta's open-sourced PDQ (256-bit, for photos) and TMK+PDQF (for video). Platforms check every upload against hash lists of known child sexual abuse material (CSAM) maintained with organisations such as NCMEC, and against shared databases of known terrorist content (e.g. GIFCT's hash-sharing database). A match is a near-certain verdict: the upload is blocked before publishing, evidence is preserved for the legally required report (US providers must report apparent CSAM to NCMEC), and the account is actioned by specialised teams. Hash matching is cheap (compute a hash, look up nearby hashes in an index), so it runs synchronously on every upload, and lists are re-applied retroactively when new hashes are added.
6.2 ML classifiers: new content
Hashes only catch known items. For everything else, ML models score each image (and sampled video frames, audio transcripts and on-image text via OCR) for policy areas: nudity/sexual content, graphic violence, hate symbols, self-harm, spam/scams, and more. Each model outputs a probability, and thresholds map scores to actions:
| Score band | Action | Why |
|---|---|---|
| Very high confidence violation | Block or remove automatically | Precision is high enough that waiting for a human only spreads harm |
| Uncertain / borderline | Publish with reduced distribution (e.g. not eligible for Explore/recommendations) and/or queue for human review | Avoid wrongly removing legitimate posts; humans decide |
| Low | Allow | Most content |
Thresholds are tuned per policy and per surface: missing CSAM is unacceptable, so recall is pushed very high and humans absorb the extra false positives; for lower-severity policies, precision matters more. Heavy models run on GPU pools in parallel with transcoding so they add little to time-to-publish; some run asynchronously after publishing, with takedown if they fire later.
6.3 Human review queues
- Items flagged by models, by user reports or by proactive sweeps land in review queues, prioritised by severity and predicted reach (a borderline post going viral jumps ahead of one with 10 views).
- Reviewers see the content with policy guidance; tools reduce harm to reviewers (blurring, greyscale, muted audio by default).
- Decisions feed back as training labels for the classifiers and as new hashes so the same content is caught automatically next time.
- Users can appeal; appeals go to a separate queue.
See Security & Privacy for abuse prevention more broadly.
7. Automatic alt text
Blind and low-vision users rely on screen readers (VoiceOver, TalkBack), which read an image's alt text. Most people never write one, so the pipeline generates it: object/scene-recognition or image-captioning models produce a description such as "Photo may contain: 2 people, smiling, beach, ocean, sunset". Facebook launched automatic alt text in 2016, and Instagram added automatic and custom alt text in 2018. The generated text is stored in the post's metadata (users can override it with their own), delivered in the API response, and — with privacy care — can be a signal for search and content understanding (see Search). It's an ML task that runs on the same GPU pool as moderation, off the critical path.
8. Cost optimisations
Media processing is one of the largest compute bills in a photo/video product, and its outputs drive storage and CDN bills. The big levers:
- Lazy / on-demand renditions: eagerly produce only the common renditions. A rare one (an unusual size for a tablet layout, a new format) is generated on first request: CDN miss → origin image service → not in storage → generate from the original (or nearest larger rendition), store it, return it, with request coalescing so a burst produces it once. You trade a slower first request for never computing or storing renditions nobody asks for.
- Spend expensive encodes only where they pay back: every video gets a cheap H.264 ladder; when a video's watch time crosses a threshold (or is predicted to), enqueue AV1/VP9/HEVC encodes. Views follow a power law, so a small fraction of videos carries most watch time. Meta engineers have publicly described this kind of prioritisation for Instagram video, producing basic encodings for everything and advanced encodings for content predicted to be watched most.
- Client-side pre-processing: the app resizes (e.g. to 1080 px) and compresses before uploading — less upload time on mobile, less server CPU. Instagram has publicly talked about starting the upload in the background while the user is still writing the caption, so "Share" feels instant.
- Deduplication: identical re-uploads (memes, reposts) detected by content hash reuse existing renditions.
- Hardware encoders and spot capacity for high-volume and batch work; efficient libraries (libvips, shrink-on-load).
- Storage hygiene: expire raw uploads; keep originals in cheaper tiers; drop renditions that can be regenerated (see Object Storage).
Is an AV1 encode worth it for one video? (all inputs are assumptions)
AV1 encode cost: ~10 core-minutes ≈ 0.17 core-h × $0.03/core-h ≈ $0.005
Saves ~40% of bytes vs H.264: an 8 MB watch → saves ~3.2 MB per view
CDN cost ≈ $0.01/GB → saving ≈ $0.000032 per view
Break-even ≈ 0.005 / 0.000032 ≈ 160 views
⇒ encode AV1 for videos likely to exceed a few hundred views; skip it for the long tail.
(Users on slow networks also get better quality at the same bitrate — a product win, not just cost.)
9. The post state machine
Many asynchronous steps must end in one consistent answer to "can people see this post?". A state machine stored with the post's metadata makes that explicit:
- Transitions are conditional (compare-and-set):
UPDATE posts SET state='PUBLISHED' WHERE id=? AND state='PROCESSING'. A duplicate "processing done" event, or a late one for a post the user already deleted, changes nothing. - The publish gate fires when the required outputs exist and blocking checks have passed; non-critical outputs (extra rungs, AV1, alt text) may arrive after PUBLISHED.
- Timeouts: a sweeper moves posts stuck in PROCESSING too long to retry or FAILED; UPLOADING posts whose upload URL expired become ABANDONED and their raw objects are cleaned up.
- Side effects on entering PUBLISHED: emit
post.created(via the outbox — see Message Queues) to trigger feed fan-out, search indexing and notifications. Fan-out must not happen earlier, or followers would see a post with missing media. - Optimistic UI: the author's app shows the post immediately from local data while the server is still processing; the client polls or receives a push when it's live (see Clients).
10. Numbers to reason with
All inputs are assumptions for practice (see Back-of-the-Envelope Estimation):
Photos: 100M/day ≈ 1,160/s average, ~3.5K/s at peak
per photo: decode + orient + 5 sizes × (JPEG + WebP) + AVIF for the top sizes + hashes
≈ 1.5 core-seconds → peak ≈ 3.5K × 1.5 ≈ 5,000 cores busy
outputs ≈ 12 files, ~0.5 MB total per photo → ~50 TB/day of image renditions
Videos: 20M/day, average 30 s
cheap H.264 ladder (5 rungs) in software ≈ 2 core-minutes per video
20M × 2 = 40M core-min/day ÷ 1,440 min ≈ 28,000 cores on average (peaks higher)
AV1 for EVERY video at ~10× cost → ~280,000 cores ✗
AV1 for the ~5–10% of videos that get most watch time → +14K–28K cores ✔
(hardware encoders/ASICs cut these core counts dramatically)
storage: ladder ≈ 30 MB + original ≈ 30 MB → ~1.2 PB/day for video
Latency targets (time from upload complete → visible):
photo: ~1–2 s 30 s Reel: first rungs in ~5–15 s, remaining rungs/codecs minutes later
11. How Instagram uses the media pipeline
- The app prepares media on-device (crop to the allowed aspect ratio, resize toward 1080 px, compress) and starts uploading via the Upload Service's pre-signed URL, often before the user finishes the caption.
- The Upload Service publishes
media.uploaded; the post sits inPROCESSING. - Media Processing Workers validate, strip EXIF/GPS, fix orientation, run hash matching and classifiers, produce image renditions or the video ladder + HLS/DASH segments, thumbnails, BlurHash/preview, and alt text — writing everything to object storage under immutable keys.
- When required outputs and blocking checks are done, the post becomes
PUBLISHEDandpost.createdtriggers feed fan-out, search indexing and notifications. - Viewers receive JSON with placeholders and signed CDN URLs per rendition; the app picks format and size, and the video player runs ABR over the CDN.
12. Trade-offs & common pitfalls
✅ What the pipeline buys
- Fast loads and smooth playback on every device and network
- Huge bandwidth savings (renditions + modern codecs)
- Privacy (no GPS leaks) and security (malformed files rejected)
- Platform safety via hashing, ML and human review
- Accessibility via alt text
⚠️ What it costs
- Large compute bill (video encoding dominates) and storage for many renditions
- Time-to-publish latency and eventual consistency
- Complex orchestration: DAGs, retries, partial failures
- Codec support fragmentation across devices
- Moderation errors either way: false positives anger users, false negatives cause harm
Common pitfalls
- Stripping EXIF before applying orientation → sideways photos.
- Forgetting metadata in other places (XMP, video container tags) → GPS leaks. Re-encode from pixels.
- Decoding untrusted files unsandboxed or without pixel limits → RCE or decompression-bomb OOMs.
- Misaligned keyframes across rungs → glitches or stalls when ABR switches.
- Publishing before moderation/renditions are ready → broken images in followers' feeds or harmful content going live.
- Non-idempotent tasks (random output names) → duplicates and orphaned objects on retry.
- One queue for everything → a bulk re-encode backlog delays users' posts.
- Encoding every video with the most expensive codec → compute cost explodes for videos nobody watches.
13. Real-world technologies
| Tool | What it does | When to use |
|---|---|---|
| libvips, sharp (Node), Pillow-SIMD, ImageMagick | Image decode/resize/encode | libvips/sharp for high-throughput, low-memory pipelines; ImageMagick for breadth (sandbox it) |
| mozjpeg, libwebp, libavif, libjxl | Format encoders | Best size/quality for each format |
| FFmpeg (+ x264, x265, libvpx, SVT-AV1) | Swiss-army knife for probing, transcoding, packaging | Almost every video pipeline |
| NVIDIA NVENC, Intel QSV, custom ASICs (Meta MSVP, Google Argos) | Hardware encoding | High-volume, real-time or cost-sensitive transcoding |
| Shaka Packager, Bento4, GPAC | HLS/DASH/CMAF packaging, DRM | Producing manifests and segments |
| AWS Elemental MediaConvert, Mux, Cloudflare Stream | Managed video transcoding/streaming | You don't want to run encoding fleets |
| Cloudinary, imgix, Thumbor | On-the-fly image transformation services | Lazy renditions via URL parameters |
| PhotoDNA, PDQ, TMK+PDQF | Perceptual hashing for known harmful content | Mandatory baseline for any user-generated media platform |
| AWS Rekognition, Google Cloud Vision, Hive, in-house models | ML classification, labels, OCR, captions | Moderation and alt text without (or before) building your own models |
| Temporal, AWS Step Functions, Netflix Conductor | Durable workflow/DAG orchestration | Multi-step pipelines with retries and state |
| BlurHash, ThumbHash | Compact image placeholders | Instant visual placeholders in API responses |
14. Interview corner
Design the photo processing pipeline for Instagram.
Client uploads directly to a raw bucket via a pre-signed URL; the Upload Service publishes media.uploaded and sets the post to PROCESSING. A worker pool consumes it: validate (magic bytes, size/pixel limits, sandboxed decode), apply EXIF orientation then strip all metadata, compute perceptual hashes and check against known-bad lists (block on match), produce renditions (e.g. 1080/640/320 px + thumbnails) in JPEG and WebP/AVIF with tuned quality, generate BlurHash and alt text, run classifiers. Outputs go to object storage under deterministic keys; a conditional state update to PUBLISHED triggers post.created for fan-out. Tasks are idempotent and retried with backoff; failures go to a DLQ and FAILED state. Pools autoscale on lag; critical renditions run in a high-priority lane.
How does adaptive bitrate streaming work and what does the backend have to produce?
The backend transcodes each video into a ladder of renditions (e.g. 240p–1080p at increasing bitrates), with keyframes aligned at fixed intervals, cuts each into 2–6 s segments, and writes HLS/DASH manifests listing variants and segments. The player downloads the manifest and then picks a rendition per segment based on measured throughput and buffer level, stepping down before the buffer empties and back up when bandwidth recovers. Segments are static files cached by the CDN.
Transcoding a 10-minute 4K video takes 20 minutes on one machine. How do you make it fast?
Split it at keyframes into chunks (say 10 s), encode every chunk of every rung in parallel across a worker pool (60 chunks × 5 rungs = 300 tasks), then stitch and package. Use hardware encoders for the first-available rungs, publish as soon as a playable subset (e.g. 360p/720p) is ready, and add higher rungs and advanced codecs later at lower priority. Ensure consistent encoder settings and closed GOPs so chunks join seamlessly.
How do you prevent known illegal images from ever being published?
Compute robust perceptual hashes (PhotoDNA, PDQ; TMK+PDQF for video) synchronously during processing, before the post can transition to PUBLISHED, and look them up against industry hash lists with a Hamming-distance threshold. A match blocks publication, preserves evidence, triggers the legally required report and account action through a specialised workflow. Re-scan existing content when new hashes arrive. ML classifiers and human review cover previously unseen content.
Video encoding costs are exploding. What do you do?
Encode a cheap baseline (H.264 ladder, possibly fewer rungs, hardware encoders) for every upload; reserve expensive codecs (AV1) and high-effort presets for videos predicted or observed to get high watch time, since views follow a power law; generate rare renditions lazily; use per-title encoding to avoid wasting bits on simple content; run batch work on spot capacity; dedupe re-uploads; and move to dedicated hardware encoders at scale. Measure with cost per watch-hour and quality metrics (VMAF), not just per-upload cost.
Why must the pipeline be idempotent, and how do you make it so?
Kafka delivers at least once and workers crash mid-task, so tasks will run more than once. Use deterministic output keys derived from (media_id, rendition, version), so re-running overwrites identical bytes; check for existing outputs before working; record task completion and post state with compare-and-set updates; and dedupe side effects (like emitting post.created) via the state transition — only the transition into PUBLISHED emits, and it can only happen once.
15. Check your understanding
Why does a portrait iPhone photo show sideways after a naive "strip all EXIF" step?
The phone stored the pixels in sensor orientation and recorded the needed rotation in the EXIF Orientation tag. Stripping the tag without first rotating the pixels loses that instruction. Fix: apply the orientation to the pixels, then strip.
What is the difference between a codec and a container? Give one example of each.
A codec compresses the media stream (H.264, AV1, AAC); a container packages encoded streams with timing/metadata into a file (MP4, WebM, MPEG-TS). The same MP4 container can hold H.264 or AV1 video.
A 720p rung is encoded at 2,000 kbps video + 128 kbps audio. How big is a 45 s clip?
2,128 kbps × 45 s = 95,760 kbit ≈ 95.8 Mbit ÷ 8 ≈ 12 MB.
Why must keyframes be aligned across all rungs of the ladder?
A player can only start decoding at a keyframe. ABR switches rendition at segment boundaries; if every rung has a keyframe at exactly the same timestamps (each segment starts with one), the player can switch cleanly. Misaligned keyframes cause glitches, stalls or the need to download overlapping data.
Two images: one is a re-saved, slightly cropped copy of the other. Will SHA-256 detect them as the same? Will PDQ?
SHA-256: no — any byte change produces a completely different hash. PDQ: likely yes — perceptual hashes of visually similar images differ in few bits, so the Hamming distance falls under the match threshold.
Using the break-even estimate in section 8, should you AV1-encode a Reel that will be watched ~40 times? ~10,000 times?
At ~160 views break-even: 40 views — no (encode cost exceeds bandwidth saved); 10,000 views — yes, savings are ~60× the encode cost, and viewers on slow networks get better quality.
A "media processed" event arrives twice for the same post; the user deleted the post in between. What should happen, and what mechanism guarantees it?
Nothing should change: the post must stay DELETED and no post.created should be emitted. A conditional state transition (… WHERE state = 'PROCESSING') fails for a DELETED post and for an already-PUBLISHED post, making the handler idempotent.
- Never serve the original: validate → strip EXIF (after fixing orientation) → resize → re-encode into multiple sizes and formats, plus thumbnails and BlurHash placeholders.
- Video = codec inside a container, transcoded into a bitrate ladder, segmented for HLS/DASH, played with ABR; align keyframes.
- Orchestrate with Kafka → worker pools → task DAG, idempotent outputs, retries/DLQ, priority lanes, autoscaling and hardware encoders.
- Moderation layers hash matching (PhotoDNA/PDQ), ML classifiers and human review; ML also provides alt text.
- A conditional post state machine gates publishing; spend expensive encodes only where views justify them.
Related lessons
- Media Upload Service — how the raw file gets into storage
- Object Storage — where originals and renditions live
- CDN — how renditions and video segments reach viewers
- Message Queues & Kafka — the events and worker pools driving the pipeline
- Clients — choosing renditions, placeholders, optimistic UI and players
- Resilience Patterns — retries, backoff and idempotency
- Security & Privacy — sandboxing, metadata privacy and abuse prevention
- Analytics & ML — models behind moderation, alt text and encoding decisions