Problem RestatementProblem
Design a system that finds harmful content on a social platform. For example (asked at Meta): posts or ads selling weapons, as well as hate speech, nudity, spam and scams. Content can be text, images or video. The system checks new posts when they are uploaded. Clear violations are blocked, uncertain ones go to human reviewers, and users can report posts and appeal decisions.
The core tension: if we block too much, innocent people get hurt (false positives); if we block too little, harmful content spreads (false negatives).
RequirementsRequirements
1.1 Functional
- Score every new post, comment, ad and profile for multiple policies (weapons, hate, nudity, spam, ...).
- Take actions: allow, reduce reach, blur with a warning, remove, ban the account.
- Human review queues with tools and priorities.
- User reports and appeals.
- Re-scan old content when policies or models change.
1.2 Non-Functional
- Low latency at upload for high-risk checks (a few hundred ms), so bad content doesn't go live even briefly.
- Scale: billions of items per day.
- Accuracy tuned per policy (weapons sales: high recall; borderline humor: careful).
- Auditability: why was this removed?
1.3 Scale Estimates
- 2B new items/day ≈ 23K/sec, peaks of 100K/sec.
- Images: ~40% of items. Video is fewer but much heavier (sample frames).
- If 0.5% go to human review, that's 10M reviews/day, too many. So ML must handle most cases, and humans handle the uncertain middle.
1.4 API Design
/v1/moderate{ content_id, type, text, media_urls, author_id } → { decision: allow|hold|block, labels: [{ policy: "weapons_sale", score: 0.93 }] }/v1/reports{ content_id, reason }/v1/appeals{ content_id, message }High-Level ArchitectureArchitecture
2.1 Overview
- Fast inline checks (at upload): hash matching against known bad content (e.g., PhotoDNA-style perceptual hashes), blocked keywords, and a fast text and image classifier.
- Async deep checks: heavier multimodal models (text + image + video frames + author signals), run within seconds to minutes after posting.
- Decision engine: combines scores with per-policy thresholds and account history, then chooses an action.
- Review system: queues by policy, language and severity, with a reviewer UI.
- Feedback loop: reviewer decisions become training labels.
2.2 Architecture Diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
UP["New post / ad"] --> FAST["Fast checks: hash match, keywords, light model"]
FAST -->|"clear violation"| BLK["Block"]
FAST -->|"publish"| PUB["Post goes live"]
FAST --> K[("Kafka - content events")]
K --> DEEP["Deep multimodal models"]
DEEP --> DEC["Decision engine - thresholds per policy"]
RPT["User reports"] --> DEC
DEC -->|"high score"| ACT["Remove / reduce reach"]
DEC -->|"uncertain"| RQ[("Human review queues")]
RQ --> REV["Reviewers"]
REV --> ACT
REV --> LBL[("Labels for retraining")]
LBL --> TRAIN["Model training"]Data ModelData model
content_scores: content_id, model_version, policy, score, created_at
decisions: content_id, action, policy, source (model|reviewer|appeal), actor, reason, created_at
review_tasks: task_id, content_id, policy, priority, language, status, assigned_to, sla_due
known_hashes: hash, policy, source -- known violating images/videosKey FlowsFlows
4.1 A new post with an image of a gun for sale
- Inline: check the perceptual hash against known bad images. No match. The text classifier sees "selling", "DM for price" and a weapon-related term, giving a weapons-sale score of 0.7, which is not high enough to block instantly.
- The post is published (or held briefly, for high-risk surfaces like ads) and an event is sent to Kafka.
- Deep model: an image model detects a firearm, the text is a sales intent, and the account is new with a pattern of similar posts. The combined score is 0.95.
- The decision engine applies the weapons-sale threshold (block at 0.9) → remove and notify the author with the reason and an appeal link.
- If the score were 0.6–0.9, it would go to the review queue instead, with priority based on predicted reach (viral posts first).
4.2 User report
Reports add a signal and can push content into review sooner. Many reports from trusted reporters raise priority.
Deep Dive A — What to do with a score of 0.7Deep dive
The classifier returns a number. Turning that number into an action is where a moderation system is actually designed, and where it most often goes wrong.
One threshold, remove above it
Pick 0.8. Anything scoring higher is deleted, anything lower is published.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
S["Score 0.79 - weapons sale"] --> PUB["Published - real harm"]
S2["Score 0.81 - satire about weapons"] --> DEL["Deleted - wrongful removal"]
DEL --> APP["Appeal, press coverage, reinstated a week later"]One number has to serve every policy at once, and the policies want opposite things. Spam and nudity tolerate false positives; political speech and satire do not. A single threshold guarantees you are too aggressive somewhere and too lax somewhere else, and there is no setting that fixes both.
A threshold per policy
Tune each policy separately against labelled data. Weapons sales lean towards recall — catch nearly all of them, accept extra false positives. Satire and commentary lean towards precision — only act when confident, accept that some violations get through.
Much better, and still binary. Every piece of content is either published untouched or destroyed, so the whole uncertain middle — which is most of the volume — has to be forced to one side. Setting the weapons threshold for high recall now deletes a lot of legitimate posts, and there is nothing softer to do with them.
Graded actions, and measure the outcome rather than the model
Give the score a range of responses instead of a switch:
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
SC["Policy score"] --> H{"Very high"}
H -->|"yes"| RM["Remove and notify - clear violation"]
H -->|"no"| M{"Moderate"}
M -->|"yes"| SOFT["Reduce reach, add a warning interstitial"]
M -->|"no"| L{"Uncertain, high reach"}
L -->|"yes"| Q["Human review queue - ranked by views at risk"]
L -->|"no"| PUB["Publish"]
SOFT --> QReducing reach is the option the binary design cannot express, and it carries most of the value: a borderline post that reaches fifty people instead of fifty thousand has had its harm cut by three orders of magnitude without anyone's speech being deleted. The review queue is ranked by views at risk, not by arrival time, because reviewer capacity is fixed and a post with two views can wait.
Then measure the system, not the classifier. Prevalence — sample content randomly every day, have experts label it, and estimate what share of views were of violating content — is the number that tells you whether moderation works. Model precision and recall are inputs to that; they are not the goal, and they can both improve while prevalence gets worse.
Deep Dive B — Adversaries and scaleScale
- Evasion: people misspell words, put text in images, crop or recolor images. Countermeasures: OCR on images, perceptual hashes that survive small edits, embeddings that capture meaning, and account-level signals (new account, many similar posts, links to known bad groups).
- Video: sample frames (e.g., 1 per second plus scene changes) and audio transcripts, and scan the most-viewed videos more deeply.
- Model updates: when a new model or policy ships, re-scan recent content in the background, starting with the most-viewed.
- Reviewer wellbeing and quality: blur by default, limit exposure time, and double-review a sample to measure reviewer accuracy.
Trade-offs & AlternativesTrade-offs
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| When to check | Fast inline + deep async | Low latency, deep coverage | Everything inline: slow uploads |
| Decisions | Per-policy thresholds + graded actions | Fits different harms | One global threshold: poor fit |
| Humans | Review only the uncertain middle | Scales, keeps quality | Review everything: impossible |
| Known content | Perceptual hash matching | Instant, precise for re-uploads | Model only: misses exact known items less reliably |
Common Follow-up QuestionsFollow-ups
- "Ads vs posts?" Ads are paid and higher risk, so review them before they go live. Posts go live and are checked right after, unless they're high-risk.
- "Multiple languages?" Multilingual models, plus review queues routed by language.
- "How do appeals work?" A different reviewer re-checks the item. If overturned, restore the content and add the case as a training label.
Wrap-UpWrap-up
Run fast checks at upload (hash matching, keywords, a light model) to stop obvious violations, then deep multimodal models asynchronously. A decision engine applies per-policy thresholds to allow, limit, remove or send to human review, prioritized by reach. Feed reviewer decisions back into training, fight evasion with OCR, perceptual hashes and account signals, and measure success with sampled prevalence.