Design Pinterest
Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.
Open with this
Save images to boards, browse a personalised grid and search by picture: 100 M daily users, 5 B distinct images behind 100 B saves. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.
The clock
- 4 min: functional requirements and scope
- 4 min: non-functional requirements, with numbers
- 5 min: back-of-envelope estimates
- 16 min: high-level design and one or two flows
- 16 min: deep dives and the close
Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.
Requirements · 8 min
Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.
Functional (9)
- Save a pin to a board: A pin is a save: an image, a link and a description, placed on one of your boards. Most saves re-save an image we already hold, so a save must never copy image bytes.
- Boards and follows: Create boards, follow people or single boards. Followed pins are one slice of the home feed, not the whole of it.
- Home feed: A personalised masonry grid, 25 pins a page, scrolling without end. Most pins come from people the viewer does not follow, and the grid must lay out before any image arrives.
- More like this: Open a pin and see related pins below it. The answer depends on the pin far more than on the viewer, which is what makes it cacheable.
- Text search: “brass floor lamp” finds pins whose words match, and also pins whose picture matches but whose description only says “my reading corner”.
- Visual search: Tap a detected object or draw a box on any image, or point the camera, and get visually similar pins: the lamp in someone’s living-room photo.
- New pins discoverable fast: A fresh pin shows in followers’ feeds and in search within minutes, even though the big indexes are rebuilt only daily or weekly.
- Spam and low quality kept out: Pins that link to spam, malware or cloaked pages are blocked or removed, and a removal applies under every save of the image, on every surface, within minutes.
- Out of scope: Ads, shopping catalogues and checkout, messaging, notifications, video pins (see Design YouTube) and the upload mechanics themselves (presigned direct uploads, as in Design Instagram). Models are named, not designed.
Non-functional (8)
- Scale (100 M DAU · 50 k pages/s peak): About 75 feed pages are served for every save, and ~100 B saves point at only ~5 B distinct images. Reads and reuse dominate.
- Home feed latency and availability (p99 < 300 ms · 99.95 % served): The first page of a session runs the whole candidate funnel; later pages read a cached ranked list in ~20 ms. When a source fails the page is thinner, never blank.
- Graph candidates from RAM (walk p99 < 50 ms): This drives the hardest trade-off. A random walk is tens of thousands of dependent memory reads, so the whole graph must sit in one machine’s RAM: ~100 B saves are pruned to ~20 B edges, and the graph scales by replicas, not shards.
- Visual search (p99 < 500 ms over ~10 B vectors): Raw float vectors are ~10 TB, so recall is traded against memory through quantisation, and a re-rank on full vectors wins most of it back.
- Grid lays out before images load (zero layout shift): Every pin in an API response carries its image’s width, height and dominant colour, so the client places all 25 cards and paints coloured tiles before the first image byte arrives.
- Freshness (new pin searchable < 5 min): In followers’ feeds within ~10 minutes, walkable as soon as it is re-saved, in the embedding index after the next daily build. Pins are evergreen, so a day is acceptable on the slow paths.
- Image delivery and durability (CDN hit > 97 % · no lost originals): ~2.4 PB of images a day leaves the edge. Originals are kept with object-storage durability; the fixed sizes can always be regenerated from them.
- Safety (removed pins off every surface < 5 min): A takedown or spam verdict on an image must apply to all of its saves in feeds, search, related pins and caches, not only to the pin someone reported.
Estimates · 5 min
Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”
The numbers (8)
- Home feed pages: ~17 k/s avg · ~50 k/s peak. 100 M DAU × 15 pages a day (25 pins each) = 1.5 B/day ÷ 86 400 ≈ 17 k/s; the evening peak runs ~3× that, ~50 k/s. Only the first page of a session runs the candidate funnel, about 1 page in 5, so ~10 k funnels a second at peak.
- Saves: ~230/s avg · ~700/s peak. 20 M saves a day ÷ 86 400 ≈ 230/s, ×3 at peak. About 90 % re-save an image we already hold, so only ~2 M a day bring new bytes. Writes are not the hard part: 75 pages are read for every save.
- Image storage: ~3 PB deduplicated (~60 PB per save). Per canonical image: an original of ~400 KB plus three fixed widths, 236 px 20 KB, 474 px 60 KB and 736 px 120 KB, ≈ 600 KB. 5 B × 600 KB = 3 PB. A copy per save would be 100 B × 600 KB = 60 PB. Growth is 2 M new images × 600 KB ≈ 1.2 TB/day, ~440 TB a year.
- CDN egress: ~2.4 PB/day · ~80 GB/s peak. Grid: 1.5 B pages × 25 pins × 60 KB (the 474 px size) ≈ 2.25 PB; close-ups: 1 B a day × 120 KB ≈ 0.12 PB. Total ~2.4 PB/day ≈ 28 GB/s on average, ~80 GB/s at peak. At a 97 % hit rate the origin still serves ~0.8 GB/s. The head is small: the top 1 % of images (50 M) × 200 KB of sizes is ~10 TB, which fits on every large PoP.
- Pin-board graph in memory: ~800 GB raw · ~185 GB pruned. 100 B save edges stored in both directions (image → boards, board → images) at 4 B per entry = 800 GB, and 7 B nodes do not even fit 32-bit ids. Pruned to ~3 B nodes and 20 B edges: 20 B × 2 × 4 B = 160 GB plus 3 B × 8 B offsets = 24 GB ≈ 185 GB, one 256 GB machine. Every graph server holds all of it.
- Walk compute: ~170 cores at peak. Walks run for ~10 k feed funnels/s plus ~7 k related-pin cache misses/s = 17 k/s. A walk of ~50 k steps at ~200 ns a step (two dependent cache misses) is ~10 ms of one core: 17 k × 10 ms = 170 cores, a handful of 64-core machines per region. The graph tier is small in machines and large in RAM per machine.
- Visual index: ~10 TB raw · ~720 GB quantised. ~5 B whole images plus ~5 B detected objects = 10 B vectors × 256 dims × 4 B = 10 TB of float32, about 80 machines of RAM for one replica. Product quantisation to 64 B plus an 8 B id = 72 B each, 720 GB: 24 shards of 30 GB in RAM, with full vectors on local NVMe for re-ranking.
- Feed cache: ~110 GB. At peak ~10 k sessions start a second, and each ranked list lives 30 minutes: 18 M live lists. 200 ranked entries × ~24 B (pin id, image id, score, source) plus a seen set ≈ 6 KB. 18 M × 6 KB ≈ 110 GB: a small Redis cluster, and all of it can be recomputed.
High-level design · 16 min
Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.
Components (14)
- Pinterest app (masonry grid · web + mobile): Renders the masonry grid: it places every card from the width, height and dominant colour in the JSON before any image loads, then fills tiles from the CDN at the width its column needs. Saves pins, batches close-ups, clicks and hides as events, and runs visual search on a crop or a camera photo.
- Image CDN (fixed widths · immutable URLs): Serves every image byte, ~2.4 PB a day. URLs carry the canonical image id and a fixed width (236, 474 or 736 px, or the original), so they never change and cache for a year. Deduplication pays off here too: an image saved 100,000 times is one URL and one cache entry, not 100,000. Misses go through a regional shield that coalesces them before object storage.
- Pin service (saves · boards · events): The write API behind the gateway (not drawn): save, re-save, create board, follow, and the engagement events the app batches. A re-save copies the image id from the pin being saved; a save with a new URL or new bytes asks the image service for the canonical image first. Runs the cheap save-time spam checks (account age and velocity, rate limits, link domain reputation) and relays pin events to the bus through an outbox.
- Image service (canonical images · pipeline): Owns the canonical image registry and the media pipeline. Synchronously: normalised source URL, then SHA-256 of the decoded pixels, to an existing image id or a new one. Asynchronously: the three widths, width, height and dominant colour, a 64-bit perceptual hash, object detection, embeddings for the image and each object, and a near-duplicate check that folds resized copies into the canonical image under an alias. One record per distinct image, ~5 B of them.
- Object storage (S3 class · ~3 PB): Originals and the three widths, keyed by canonical image id and width: ~600 KB per image, ~3 PB in total, growing ~1.2 TB a day. Written once and never modified, which is what lets the CDN cache forever. Old originals move to a colder tier; the sizes can always be regenerated from them.
- Pin store (sharded MySQL · key=user_id): Pins (image id, board, link, description, and the image’s width, height and colour copied in at save time), boards and follows, sharded by owner so a board page or a profile is one shard. A pin row is ~300 B, ~30 TB for 100 B pins. A unique key on (board, image) makes a repeated save a no-op.
- Event bus (Kafka · key=image_id): pin.saved, pin.removed and image.removed, plus the app’s engagement events. Graph servers, the search indexer and the offline builds each consume it at their own pace, so a slow consumer never slows a save. Pin events are keyed by image id, so every save of one image is processed in order by one consumer.
- Home feed service (candidates · rank · blend): Builds the home feed and “more like this”. On the first page of a session it gathers ~2,000 candidates from graph walks seeded by the user’s recent pins, followed boards and people, and embedding neighbours, has the ranker score them, then blends for diversity (one card per image, at most two per board, a cap per link domain) and caches ~200 ranked pins for 30 minutes. Later pages are reads of that list, and each request carries what the user engaged with, so the next refill uses it.
- Feed cache (Redis · TTL): Three short-lived key families: each session’s ranked list (~6 KB, 30 minutes, ~110 GB at peak), each board’s newest 50 pin ids for its followers (10 minutes), and each canonical image’s related-pin candidates (~500 ids, 6 hours). Everything in it can be recomputed, so a lost node costs latency, never data.
- Graph walk service (pin-board graph in RAM): Holds the whole pruned bipartite graph of canonical images and boards (~3 B nodes, ~20 B edges, ~185 GB) in RAM on every server, as compressed adjacency arrays in both directions, and answers random-walk queries in ~10 ms of CPU, the approach of Pinterest’s published Pixie system. Sharding would put most steps of a walk on the network, so it scales by replicas. A daily snapshot plus an in-memory overlay of today’s saves from the bus makes a new save walkable within seconds.
- Model serving (embedding ANN · GPU ranker): Two things the feed calls. A nearest-neighbour index of graph-aware image embeddings, in the spirit of Pinterest’s PinSage paper (each image embedded from its own visual and text features and its graph neighbourhood): ~2 B recommendable images compressed to ~144 GB across 6 shards. And a GPU ranker that predicts save, close-up, click-through and hide for ~1,000 candidates, on engagement aggregated across every save of an image. Both are loaded daily from the offline builds.
- Search service (text tiers · query encoders): Text and visual search, ~7 k and ~400 queries a second at peak. Holds the inverted index, one document per canonical image built from every save’s description and board name plus the link’s page title, as a daily main tier and a real-time tier fed from the bus. Embeds text queries and image crops into one shared image space, queries the visual index, fuses lexical and vector candidates, filters removals and ranks.
- Visual index (IVF-PQ · ~10 B vectors): Embeddings of ~5 B whole images and ~5 B detected objects in one image space, which a text encoder is also trained into: 10 TB as float32, ~720 GB as 64-byte PQ codes, held in RAM in 24 shards by image id with full vectors on NVMe for re-ranking. Rebuilt weekly with fresh centroids and codebooks; an HNSW fresh tier holds the last 7 days in full precision, and every query reads both.
- Offline builds (daily · weekly · versioned): Daily: snapshots the pin store into the pin-board graph, prunes it and ships it to the graph servers; recomputes image embeddings and the ranker; aggregates engagement onto canonical images; rebuilds the main text tier; scores spam and quality per image, board, account and link domain. Weekly: rebuilds the visual index. Every artefact is versioned, loaded beside the old one and checked before it replaces it.
Flows to ask them to walk (5)
- Save an image from the web: The core write. Most of the work is deciding whether we already hold this image, because 90 % of the time we do, and a save must cost a row, not a copy.
- The user saves an image from a recipe site to their “Weeknight dinners” board.
- The pin service asks the image service for the canonical image behind that URL.
- For an image never seen before, it stores the original and writes the three fixed widths.
- The pin row is written with the image’s size and colour copied in, and the save returns.
- In the background the image service detects objects, embeds the image and each object, and adds them to the visual index’s fresh tier.
- A near-duplicate check on the new vectors folds the image into an existing canonical one if it is the same picture.
- The pin event reaches the graph servers, which add the edge to today’s overlay, and the search indexer.
- Open the home feed: The main read. Candidates come mostly from the graph, not from follows; they are ranked and blended once per session and served in pages from a cache. Every card carries enough metadata to lay out the masonry grid before a single image loads.
- The app asks for the first page of the home feed.
- The feed service picks query pins from the user’s recent activity and asks the graph for random walks from them.
- In parallel it gathers recent pins from followed boards and people, and embedding neighbours.
- Candidates are merged by canonical image and filtered, and the ranker scores about 1,000.
- The blender orders them for diversity and caches about 200 ranked pins for the session.
- The first 25 are hydrated in one batched read and returned with width, height and colour.
- The app loads each tile from the CDN at the width its column needs.
- Scrolling reads the next 25 from the cached list, and engagement rides along on the request.
- A pin goes viral: more like this: The scale-breaking case. One image is saved 100,000 times in a day and opened millions of times. Because a pin is a save of a canonical image, it stays one record, one URL and one cache entry; the risk moves into the graph, where the image becomes a hub.
- Thousands of people save the same image; each save is a new pin row and one more edge on the same canonical image.
- Millions open it; every close-up is the same CDN URL at 736 px.
- The close-up asks for more like this, and the image’s related-pins candidates are a cache read.
- On a miss, the feed service walks from this one image and adds embedding neighbours.
- Inside the graph the viral image is a hub, so its degree is capped and its visits are discounted.
- Engagement on all 100,000 pins is summed onto the canonical image for ranking.
- Visual search, with a slow shard: Search over ~10 B vectors that only fit in RAM once quantised. The query fans out to 24 shards and one of them is slow; the design still returns a good answer on time.
- The user taps the lamp in a living-room pin, or draws a box around it.
- The search service embeds the crop into the shared image space.
- It queries all 24 shards of the main tier and the fresh tier in parallel.
- One shard is slow: after its p95 the request is hedged to a replica, and at the deadline the merge goes ahead without it.
- Hits are merged, grouped by canonical image and filtered.
- The top results are ranked on similarity, quality and engagement and returned with layout metadata.
- A new pin is found; a spam pin vanishes: The background path. The big indexes are rebuilt daily or weekly, so freshness comes from small real-time tiers merged at query time, and removal comes from a filter applied after the merge rather than from editing the big indexes.
- At 10:00 someone saves a brand-new photo of a brass floor lamp, described as “my reading corner”.
- Within seconds it is in the real-time text tier and the visual fresh tier.
- At 10:04 a user searches “brass floor lamp”; the text index and the query embedding run side by side.
- Overnight the builds fold the day into the main tiers and trim the real-time ones.
- Meanwhile a burst of day-old accounts saves one image 3,000 times, each with a link to a different new domain.
- The removal is applied once, to the canonical image, and reaches every surface as an event.
Deep dives · 16 min
Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.
A pin is a save of an image
Ask: Why not just store each pin with its own copy of the image?
Good answers name: Canonical image by exact hash, near-duplicates folded in by perceptual hash, pins referencing it, A copy of the image per pin, Exact-hash deduplication only, Cluster by embedding similarity alone.
Our pick: Three levels at save time. A normalised source URL lookup answers most web saves without a download; a SHA-256 of the decoded pixels answers exact copies; and an asynchronous near-duplicate check in the first minute takes the nearest neighbours from the visual index and accepts one only if the 64-bit perceptual hashes differ in 6 bits or fewer and the aspect ratios agree within 1 %. A match means the same picture at another size or quality: the new id becomes an alias of the existing canonical image, which keeps its URLs and sizes unchanged, and the few pins created in that minute are re-pointed. A crop, a watermark or overlaid text changes the picture, so those stay separate images linked as similar, and the feed’s blender keeps similar images apart. Pins hold the image id plus the width, height and colour copied in, and live on their owner’s shard; the canonical image record holds the hashes, sizes, detected boxes, quality and spam scores, and the representative pin used when the image is shown without a particular save.
- Two different photos happen to have perceptual hashes 5 bits apart. What stops a false merge?
The hash alone never merges. The candidate must also be among the nearest neighbours in the visual embedding space and have the same aspect ratio within 1 %, and very low-detail images (a solid colour, a plain logo), whose hashes collide easily, are excluded from near-duplicate merging altogether. If a false merge slips through, it is reversible: the duplicate’s original bytes are kept for 30 days, so splitting it back is a metadata change. User reports of “this is not my image” are the metric that says whether the thresholds are right. - A user deletes their pin. What happens to the image?
Nothing, unless it was the last reference. The pin row is deleted, the graph edge leaves the overlay when the event arrives and the snapshot at the next build, and the image’s save count drops at the next hourly aggregation. When the last pin goes, the image is marked unreferenced and collected after 30 days by a job that re-checks the count first, because a new save can arrive in between. - A copyright owner asks you to remove an image. It has 40,000 saves.
That is one operation on the canonical image, which is a big advantage of this model: mark it removed, publish image.removed, and every surface drops it through its removal bitmap within a minute, while its handful of CDN URLs are purged. Near-duplicates folded into it go with it automatically. Separate images linked to it as similar, such as a crop, are queued for review rather than removed blindly. - Why hash decoded pixels instead of the file bytes?
The same picture is often served with different metadata, an added colour profile or a stripped EXIF block, which changes the file hash but none of the pixels. Hashing decoded pixels catches those for the price of a decode we do anyway to make the sizes. A recompression changes the pixels slightly, so that case falls to the perceptual hash. - How does a re-save avoid the image service completely?
A re-save names the pin being saved, and that pin already knows its canonical image id, width, height and colour. The pin service copies them into the new row and writes it: no lookup, no fetch, no hash. That is the 90 % case, which is why saves are cheap even though new images are expensive.
Random walks on the pin-board graph
Ask: Why recommend from random walks over the save graph instead of only from embeddings, and how do you serve them in 50 ms?
Good answers name: Random walks with restart on a pruned graph held whole in RAM on every server, Embedding nearest neighbours only, Precomputed co-save lists per image (item-item collaborative filtering), Walks over a graph sharded across machines or in a graph database.
Our pick: A bipartite graph of canonical images and boards, built daily from a pin store snapshot and pruned: images saved only once are dropped (a walk that steps onto one can only step back), catch-all boards whose pins span too many topics are dropped, spam and low-quality boards are dropped, and hubs keep at most 10,000 board edges, the most topical ones. That takes ~100 B save edges to ~20 B edges over ~3 B nodes, small enough for 32-bit node ids and compressed adjacency arrays in both directions: ~185 GB, on 256 GB servers that each hold all of it. A query takes up to 20 weighted query pins from the user’s recent activity, splits a budget of ~50 k steps between them by weight, walks with restart, prefers boards in the user’s language and topics, boosts images reached from several query pins, divides visits by a power of the image’s degree, and stops early once enough images have high counts. Today’s saves land in an in-memory overlay from the event bus, so a re-save made a minute ago is walkable. The published Pixie system works this way (walks with restart, biasing by user features, a boost for multi-query hits, early stopping, graph pruning); the thresholds here are my own starting points.
- The graph doubles and no longer fits in one machine. Now what?
First prune harder and compress better: tighter degree caps, dropping boards untouched for years, and delta-encoded adjacency lists buy a lot before any architectural change. Next, bigger machines, which is still the cheapest option. If it truly cannot fit, split it into a few graphs by language or region that are each whole, and route a query to the user’s graph, accepting that walks across them are lost. What I would not do is shard one graph across machines, because every step would become a network hop. - A user’s recent saves are all brand-new images with no edges in the graph. Where do the walks start?
From proxies. For each query pin with no graph presence, take its nearest neighbours in the embedding index that do have edges and start the walk from those, weighted by similarity. The overlay often helps too: if the image was re-saved even once today, it already has an edge. And the embedding source contributes candidates directly, so the feed never runs dry for lack of graph signal. - A graph server crashes at the peak. What happens?
Its in-flight walks fail and the feed service retries them on another replica; any replica answers any walk, so there is nothing to rebalance. A replacement loads the current snapshot from blob storage (~185 GB, a few minutes on a fast network) and replays the day’s overlay from the bus offset recorded with the snapshot, then joins the pool. The tier runs N+2 per region, so losing one does not even move latency. - Why not count co-saves exactly instead of sampling?
Exact counts from popular images explode: an image on 100,000 boards of ~1,000 pins each reaches 100 M two-hop paths, and the counts are dominated by giant boards and hub images. A walk with a fixed step budget samples the neighbourhood in proportion to how strongly it is connected, ignores the long tail of weak paths by construction, and combines many query pins in one pass. It is an estimate, but of the right quantity at a fixed cost. - How do you know the walk candidates are any good?
Measure each source on its own. Log which source produced every impression and compare save and hide rates per source and position. Offline, replay: of the pins users saved today, what share were in the walk candidates generated for them yesterday, recall@1000 for the source. Pruning and degree-cap changes ship as experiments, because they change every walk.
Building the home feed
Ask: Most of the home feed comes from people the user does not follow. How do you build it, and why not precompute it with fan-out like Instagram?
Good answers name: Per-session funnel (walks, follows, embeddings), ranked and blended once, served in pages from a 30-minute cache, Fan-out on write of followed boards into per-user timelines, Recompute the whole funnel on every page, Precompute each user’s feed nightly.
Our pick: The first page of a session builds ~2,000 candidates in parallel: ~1,000 from graph walks seeded by up to 20 recent pins, ~800 from followed boards and people (the follow list from the pin store, each board’s newest 50 from the cache), and ~1,000 from the embedding index, with the user’s recent pins clustered into up to 5 interests. They are merged by canonical image, so an image found three ways is one candidate with three tags; filtered for saved, hidden, removed and low-quality images; trimmed to ~1,000 by a light model; and scored by the GPU ranker for save, close-up, click-through and hide. A greedy blender then fills the order: no near-identical images on one screen, at most 2 per board and 3 per link domain in any 25, a spread of topics, and followed pins between 15 and 30 %. The top ~200 are cached for 30 minutes (~6 KB each, ~110 GB at peak); pages are a read plus a hydrate, and when ~50 remain the list is refilled with the pins the user just engaged with as new query pins. New users start from the topics they pick at sign-up: walks seeded from top images in those topics, popular pins for their country and language, and a deliberately wide spread of topics, until a few saves give the walks something personal.
- Why “at most 3 per link domain”? That is not a relevance rule.
It is a diversity rule and a spam brake at once. A user who likes one recipe site would otherwise get a feed that is one site, which is worse for them, and a spammer who gets one image to rank well would otherwise get the whole page. A cap per domain costs little relevance and bounds the damage any one domain can do. The same reasoning gives the cap per board. - The user saves a pin on page 2. When does the feed react?
On the next refill: the request carries the save, and the feed service refills in the background with it as a heavily weighted query pin, early if the save is a strong signal. Pages already in the list are not re-ranked, because reshuffling what is just below the user’s thumb is jarring. The next session also starts from it. - The feed cache loses a node at peak. What do users see?
Users whose session lists lived there miss on their next page, and the feed service runs the funnel again, which is the first-page path and works. The risk is the burst of extra funnels; the walk and GPU tiers keep ~30 % headroom at peak, and if they run short the feed service builds a cheaper page (walks and follows, ordered by a light model) rather than failing. A few repeats are possible for those users, because the seen set lived with the list. - Why rank after merging by canonical image instead of ranking pins?
Because the ranker’s best features belong to the image: its save and close-up rates over every save, its quality, its topics. Ranking pins would score three saves of the same image three times and then show the same picture three times. After ranking, the blender picks which save to display: a followed person’s if there is one, since “saved by Ana” is a reason to look, otherwise the representative pin. - How would you know the feed is getting better?
Saves and close-ups per session as primary metrics, because a save is the user saying the pin was worth keeping, with hides and quick bounces as guardrails and 7- and 28-day return rates over longer A/B tests by user. Per-source save rates show which source earns its slots, and the share of followed pins is watched so a model change does not silently starve the follow graph.
Visual search over 10 B vectors
Ask: A user draws a box around a lamp in a photo. How do you find similar lamps among billions of images in a few hundred milliseconds, and what does it cost in memory?
Good answers name: IVF-PQ in RAM, 24 shards by image id, full-vector re-rank from NVMe, plus an HNSW fresh tier, HNSW over full-precision vectors in RAM, A disk-based graph index (DiskANN class) on NVMe, Partition the index by detected category and query only the matching one.
Our pick: One image space for everything: a visual encoder embeds whole images and detected objects into 256 dimensions, and a text encoder is trained into the same space so text search can use the index too. Vectors are product-quantised to 64 bytes and stored with an 8-byte id in an IVF of 65,536 lists per shard; 24 shards by image id hold ~420 M vectors and ~30 GB each in RAM, replicated three times per region for availability rather than for load. A query scans the 64 nearest lists per shard (~400 k codes, ~5 ms with SIMD lookup tables), re-scores the shard’s best 200 against full vectors on NVMe (200 random 1 KB reads, a few ms) and returns them; the search service merges, collapses hits to canonical images and ranks. Recall@100 against exact search on sampled queries is the tuning target, ~95 %, with nprobe and the re-rank depth as the dials. The weekly build retrains centroids and codebooks on a sample and re-encodes everything; the fresh tier, HNSW over the last 7 days (~28 M vectors, ~28 GB in full precision), is read on every query, so a new image is findable seconds after its vectors exist. Pinterest has published papers on its visual search (for example at KDD 2015) with the same overall shape of detecting objects, embedding them and looking up nearest neighbours; the sizes here are mine.
- Visual search traffic grows 10×. What do you add?
Replicas, not shards: at a few thousand queries a second each shard still spends only ~5 ms of scan per query, so each shard group gets more copies. NVMe re-rank reads grow linearly too, and a drive serves hundreds of thousands of random reads a second. If cost bites, lower nprobe at peak and cache results for stored boxes on popular pins, since a tap on the lamp in a viral pin is the same query every time. - Why index every detected object instead of one vector per image?
Because people search for the object, not the scene. A whole-image vector of a living room is dominated by the sofa and the walls, so a query for its lamp finds other living rooms. Object vectors double the index, but they are what makes “find this lamp” work, and stored boxes mean a tap on a pin needs no detector or encoder at query time. - You retrain the visual model. What happens to 10 B vectors?
All of them are recomputed, because vectors from two models live in different spaces. The build re-embeds every image and object from the stored originals (a large but embarrassingly parallel GPU batch), builds a complete new index version beside the old one, checks recall on sampled queries, and switches the query encoders and the index together with one version pointer. Stored box vectors on image records are versioned the same way. - How do you keep spam and removed images out of visual results?
Filter after the merge, never by editing the index: every search host holds the removal bitmap of canonical images (5 B bits, 625 MB) and drops hits against it. Quality and spam scores enter the ranking, so a spammy look-alike loses near-ties to a clean one. The next build leaves removed images out entirely, so the filtered share stays small. - What does product quantisation actually cost you in quality?
Distances from 64-byte codes are approximations, so true neighbours are often found but in the wrong order, and some fall just outside the candidate list. Re-scoring the top 200 per shard with full vectors fixes the order, and fetching 200 when we show 50 catches most of the ones PQ ranked too low. Recall@100 against exact search on a held-out sample of queries measures what is left, and nprobe and the re-rank depth are tuned against it.
Images in a masonry grid
Ask: Each page shows 25 images of different heights, 50 k pages a second. How does the grid lay out instantly, and how do you serve 2.4 PB of images a day?
Good answers name: Fixed widths generated at upload, immutable URLs on a CDN, and width, height and dominant colour in every pin, Resize on the fly at the edge for any requested width, Let the client measure each image after it loads, Crop every image to one aspect ratio.
Our pick: Generate 236, 474 and 736 px widths at upload (WebP, with JPEG copies for old clients), keep the original for zoom and regeneration, and key everything by canonical image id and width so URLs are immutable and cache for a year. Every pin in every API response carries width, height and a dominant colour (k-means over a 32 px thumbnail that ignores near-white backgrounds), so the client assigns cards to columns and paints coloured tiles from the JSON before requesting a single image; very tall images are capped at 1:3 in the grid and shown whole in the close-up. Phones ask for 474 px and desktops for 236 or 474, so the edge holds about two objects per popular image. The CDN runs edge PoPs over regional shields that coalesce concurrent misses, so a viral image costs one origin fetch per region; the top ~1 % of images (~50 M) need only ~10 TB at the edge, the hit rate stays above 97 %, and the origin serves ~0.8 GB/s on average. The format is part of the URL rather than negotiated from the Accept header, so the cache key never fragments.
- A pin goes viral in one country at 9 pm. What does the CDN see?
A spike of requests for the same two URLs. The first request at each PoP misses to the regional shield, which coalesces concurrent misses into one origin fetch, so object storage sees one read per region rather than thousands. After that every request is an edge hit; the pin’s popularity actually raises the hit rate. The thing to watch is the shield’s miss queue in the first seconds, not the origin. - Why copy the colour and dimensions into the pin row instead of reading the image record?
So a page of 25 pins hydrates with one batched read of pin rows instead of 25 pin reads plus 25 image reads. The copies can never go stale, because a canonical image never changes: an edit is a new image with a new id. The duplication is ~10 bytes per pin, ~1 TB across 100 B pins. - You add AVIF, which is ~30 % smaller. How do you roll it out?
As new URLs, not as content negotiation: generate AVIF versions of the three widths, starting with the head of popular images where the egress is concentrated, and let clients that decode AVIF request the .avif URL. The long tail can be converted lazily on first request at the shield and cached. The saving is large: ~30 % of the grid’s ~2.25 PB a day. - A photo is removed for legal reasons. How do you get it out of the CDN?
Purge its handful of URLs (three widths and the original, in each format) by key, which takes seconds across the CDN, and mark the image removed so no API response references it again. Because URLs are per canonical image, one purge covers all of its saves; with a copy per pin it would be thousands of URLs.
Fresh pins in, spam pins out
Ask: Your graph and indexes are rebuilt daily or weekly. How does a pin saved a minute ago show up in search, and how does a spam pin removed a minute ago disappear everywhere?
Good answers name: Small real-time tiers merged at query time, removals as a bitmap filter, big tiers rebuilt and swapped, Insert new items into the main indexes directly, Rebuild more often instead, Delete removed items from every index and cache.
Our pick: Each serving system pairs a big rebuilt tier with a small real-time one. The graph: a daily pruned snapshot plus an in-memory overlay of today’s saves from the bus (~20 M edges, ~320 MB), with new edges per node capped so a viral image cannot rebuild its hub during the day. Text search: a daily main tier plus a real-time tier refreshed every ~10 s. Visual search: a weekly IVF-PQ build plus a 7-day HNSW tier. Each query reads both tiers, merges by image id and lets the newer document win. Removals go the other way: image.removed and pin.removed events set bits in a removal bitmap (one bit per canonical image, 625 MB) on every feed, search and graph host, applied after every merge and to every cached list at read time, so nothing is edited and nothing is missed. Spam is decided in layers. At save time, cheap checks: account age and velocity, rate limits and the link domain’s reputation. Hourly, a model that scores images, boards, domains and accounts together, which is where deduplication pays off again: one image saved by 3,000 day-old accounts with 3,000 unrelated new domains is one obvious cluster. Continuously, a crawler checks link destinations for malware, redirect chains and cloaking. Low quality that is not spam (blurry, tiny, a dead link) lowers an image’s quality score and its rank instead of removing it.
- The daily graph build fails two days in a row. What degrades?
Little at first: yesterday’s snapshot keeps serving and the overlay keeps growing with every save, so new saves stay walkable, and at ~320 MB a day a few days fit easily. What ages is the pruning, so spam boards found since are filtered rather than pruned. Two failures page someone, because the embeddings and the main text tier come from the same snapshot and are aging too. - A spammer learns your save-time checks and buys aged accounts. What catches them?
The hourly cluster model, because it looks at what accounts do together rather than at one account: many accounts saving the same images to new boards, linking to a small set of domains, in a burst. The crawler catches the destination side: domains that redirect, cloak (show the crawler one page and users another) or serve malware get a bad reputation that applies to every pin linking to them. Aged accounts are expensive, so the attacker runs out of them too. - Why not run the full spam model synchronously on every save?
The strongest signals are collective and only exist once a burst has started: 3,000 saves of one image look like spam, the first one does not. Within a save’s latency budget only cheap, local checks are possible, and they are tuned to slow attackers rather than to decide. Blocking later costs little, because removal reaches every surface within a minute and a new pin gets little distribution in its first hour anyway. - A removal reaches the bitmap, but a user’s cached session list still holds the pin. Do they see it?
No. Cached lists are filtered against the bitmap at read time on every page, so a list cached 20 minutes ago cannot show an image removed a minute ago. That is the reason to filter at read rather than purge caches: there are millions of cached lists, and finding the ones that hold an image would be a search of its own. - How do you measure freshness?
Canary pins: a job saves a synthetic pin every minute with a unique word and a unique image, then probes text search, visual search and the graph overlay until each finds it, recording the delay per surface. The same canaries are then removed and probed until each surface stops returning them. Those two latencies per surface are the dashboards, alerted at the 5-minute target.
Close · 5 min
Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.