SwadhyAI Notes

All System Design notes

System Design Case Study

News feed

You open Instagram or Facebook on the couch, pull to refresh, and a ranked mix of friends, pages, and suggestions appears in under a second. It feels like a timeline. It is really a retrieval + ranking product with privacy guards.

A news feed is not posts sorted by time. It chooses candidates, filters private or blocked content, ranks what matters, then posts fast, and logs what you actually saw.

Open feed journey

1

Friends, recs, ads within a budget.

2

Blocks, deletes, privacy, policy.

3

Score, then diversity and inventory.

4

Objects to client; impressions async.

Try it: news feed assemble and rank

The feed mixes followed publishers, ads, and ranking features. Assembly should be cacheable per user segment where possible.

  1. Client
  2. Feed API
  3. Candidate service
  4. Ranker
  5. Cache
Highlighted hops show where the last action moved traffic. A stopped hop means backpressure, rejection, or a consistency guard blocked further progress on that path.
Assemblies
0
Cache hits
0
Mode
cold
Path hop
0

Candidates

fan-in

0 assemblies.

Cache

TTL

0 warm hits.

Freshness

rank

Breaking posts may bypass cache for a short window.

Open a cold feed assembly, then a warm cached feed for the same user.

Scope boundary: ranked discovery, not only timeline delivery

Social microblog lesson

Optimizes author-to-follower delivery: post durability, , celebrity hot partitions, timeline freshness, and cross-device consistency.

Read the microblog design →

This news-feed lesson

Optimizes discovery quality after delivery: multi-source candidates, eligibility, ranking, diversity, experiments, impression truth, and safe model rollback.

What actually happens

On read, the feed service loads ids from a (and other sources), runs filters, scores with , applies a , then a page with a . An records exposure asynchronously.

On write, posts land in durable storage. Delivery into timelines may use so celebrities do not melt push paths, but ranking quality is a separate loop: features, models, canaries, and holdbacks.

At Facebook-scale numbers (billions of users, hundreds of millions of posts/day), you must bound candidate budgets or online scoring cost explodes.

How to choose the design moves

1. Is the hard problem delivery or discovery? Pure follow-graph timelines lean on fan-out. Personalized discovery needs + .

2. Can ranking override privacy? Never. is a hard gate before and after scoring.

3. Will page 2 duplicate page 1? Ranked feeds reorder; use a and a truthful for learning.

Simple rule of thumb

Bound candidates, filter eligibility, spend rank budget wisely, hydrate with a final privacy check, log exposures honestly. If the ranker dies, serve chronological or cached candidates, never an empty private leak.

Case-study upgrade

News Feed Ranking Math

A news feed combines candidate generation, filtering, ranking, hydration, and impression logging. Correctness includes privacy and deletion, not only ranking.

Assumptions and formulas

  • Daily active users: 1B. Refreshes/user/day: 10. Posts/day: 500M.
  • Feed cache stores candidate ids; online ranking should score a bounded candidate set.
  • Visibility filtering rechecks deleted, private, blocked, and muted content at read time.

Concrete math

  • Feed reads: 1B x 10/day = 10B/day = 115.7K QPS average; peak can reach 350K to 1M QPS.
  • Posts: 500M/day = 5.8K/sec average; peak can be 20K to 60K/sec.
  • Fan-out: 5.8K/sec x 500 followers average = 2.9M timeline inserts/sec.
  • Feed cache: 1B users x 1,000 post ids x 8 bytes = 8 TB raw; with overhead plan tens of TB.
  • Ranking: 100 candidates/read x 350K peak QPS = 35M candidate scores/sec if fully online.

Design decisions forced by numbers

  • Pre-rank and cache candidates; online rank only the top bounded set.
  • Over-fetch candidates because filtering removes deleted or private items.
  • Use stable cursors so inserts and ranking changes do not duplicate or skip too much.

Scaling to millions

  • At 1M users: chronological feed cache is enough.
  • At 10M users: add fan-out workers, social graph cache, and media hydration cache.
  • At 100M+ users: add ranking pipeline, hybrid fan-out, visibility recheck, stable cursors, and degraded cached feed.

Readable flow

Candidate generation
Filtering
Ranking
Hydration
Response
Impression log

Interview follow-ups to expect

  • Where are privacy and deletion enforced?
  • How much ranking can happen online?
  • What happens when ranking features are stale?

Operational readiness checklist

After the math, explain how this capacity plan is measured, protected, launched, and evolved.

Observability

  • Attach each capacity estimate to a dashboard metric: p99 latency, queue age, cache hit rate, shard skew, error rate, and freshness lag.
  • Alert on rate-of-change and saturation, not only hard failures; backlogs and hot keys usually warn before an outage.

Security and abuse

  • Budget for auth checks, rate limits, quota isolation, audit logging, privacy filters, and abuse-review side paths.
  • Separate trusted internal events from user-controlled input so retries and backfills cannot bypass policy checks.

Degraded modes

  • Define the fallback when a dependency is slow: stale cache, partial results, read-only mode, delayed async work, or admission control.
  • Protect the invariant first; reduce freshness, personalization, or analytics quality before risking duplicate or corrupt state.

Testing and rollout

  • Test average, peak, and hot-object traffic; include retry storms, cold caches, lagging workers, and regional dependency loss.
  • Use canaries, shadow reads, dual-write validation, feature flags, and explicit rollback criteria for risky path changes.

Rollout, backfill, and runbooks

  • Make migrations and backfills idempotent, resumable, rate-limited, and visible through progress and error metrics.
  • Write the runbook before launch: owner, dashboards, paging thresholds, mitigation switches, replay steps, and repair commands.

10x evolution

  • For news feed ranking math, state which number forces the next move: cache memory, shard count, write amplification, queue lag, egress, or failover reserve.
  • Add the next layer only when the metric forces it: sharding, regional reads, async replay, hot-key isolation, or stricter backpressure.

Scope you should nail down

Product surface

A reader should receive a page of posts from followed accounts and selected recommendations. Posts can contain text, images, or video, while likes, comments, and shares create feedback for future ranking. Following changes which authors are eligible; it does not guarantee that every post appears on the first page.

Define what refresh means before choosing a cache policy. An explicit refresh may generate a new ranked session, whereas scrolling should continue the existing session without repeating posts. A new-post notification can invite a refresh without moving content while the reader is scrolling.

Scale targets

Use the following as a hypothetical workload: two billion registered users, one billion daily active users, and 500 million posts per day. These are sizing assumptions for the exercise, not measurements of a particular company. Assume a 200 ms server-side p99 target and evaluate network and media download latency separately.

Stale like counts are usually tolerable for a short period. A deleted post or newly private account has a stricter visibility requirement: a fast ranker cannot make stale authorization acceptable. Celebrity accounts also create highly uneven delivery work, so average followers alone cannot size fan-out workers.

Capacity picture

115K QPS
Feed requests (avg)
5.8K/sec
New posts (avg)
900 TB
Posts (5yr sketch)
100 TB
Feed cache order

Feed requests: 1B DAU × 10 refreshes ≈ 115K QPS avg; peak 350K–1M

Posts: 500M/day ≈ 5.8K/sec; peak 20K–60K/sec

Fan-out pressure: 5.8K/sec × 500 followers ≈ 2.9M timeline inserts/sec → async

Feed cache: 1B × 1,000 ids × 8B = 8TB raw before sorted-set overhead

Ranking load: 100 candidates × 350K QPS = 35M scores/sec if fully online: pre-rank

Social graph: 2B × 500 friends × 100B ≈ 100TB

Feed read pipeline

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
    autonumber
    participant U as User
    participant F as Feed service
    participant C as Candidate cache
    participant G as Graph filter
    participant R as Ranker
    participant H as Hydration cache
    participant I as Impression log

    U->>F: getFeed(cursor)
    F->>C: load candidate post ids
    F->>G: remove blocked/private/deleted
    F->>R: score top 100
    R->>H: batch hydrate posts + authors
    H-->>F: page objects
    F-->>U: feed page + stable cursor
    F-->>I: impression events (async)

Candidates, ranking, and the learning loop

Separate retrieval from ranking. Retrieval protects recall; ranking spends a bounded latency budget; the impression log records what was eligible and shown so offline training does not learn from false exposure.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart LR
    A[Friend and group posts] --> D[Candidate union]
    B[Recommendations] --> D
    C[Eligible ads] --> D
    D --> E[Policy and privacy filter]
    E --> F[Lightweight pre-ranker]
    F --> G[Heavy ranker]
    G --> H[Diversity and inventory mixer]
    H --> I[Hydrate and recheck visibility]
    I --> J[Serve ranked page]
    J -. impression, dwell, hide .-> K[Feedback log]
    K -. offline training .-> L[Versioned model]
    L -. canary rollout .-> G
    M[Delete or privacy event] -. invalidate .-> D
    M -. final guard .-> I

Bound the candidate budget

Suppose retrieval returns 600 friend posts, 250 recommendations, and 150 advertisements. Deduplicate those 1,000 candidates and remove ineligible items before a cheap pre-ranker selects 200 for the expensive model. The final mixer chooses a 50-item page, leaving enough extra candidates to replace items rejected by the last visibility check.

This arrangement separates recall from precision. Retrieval must find worthwhile posts at all; ranking cannot recover a post that was never retrieved. Scoring 200 instead of 1,000 candidates reduces model invocations fivefold, but an overly aggressive pre-ranker can remove the very posts the expensive model would have preferred. Measure candidate coverage alongside serving latency.

Make exposure logs truthful

An impression means the client actually displayed a post under an agreed viewability rule, such as a minimum visible area and duration. Returning a post from the server is only a delivery event: the user might close the app before scrolling to it. Assign an exposure identifier and log the model version, experiment, and displayed position when the client confirms the impression.

Join a later like or hide to that exposure identifier and deduplicate retried client events. Otherwise the training data can mark unseen posts as ignored, or count one interaction twice. Keep privacy-sensitive identifiers restricted and define retention for raw exposure data.

Experiment with guardrails

Consistent assignment, ramp 1% → 5% → 25% → 50%, concurrent holdback. Auto-stop on p99, crashes, hides/reports, diversity, or empty-feed regressions, even if clicks rise.

Correctness invariant

No ranking score may override . Deletion, block, membership, age, regional policy, and privacy run before ranking where possible and again immediately before the response.

FAANG-level hardening

Capacity and SLOs

500M posts/day ≈ 5.8K writes/sec; ×500 followers ≈ 2.9M timeline inserts/sec. Target feed p99 under 200ms and freshness lag under 60s for normal follows.

Metrics and degraded modes

Watch fan-out lag, ranking p99, candidate fill, visibility rejects, cursor replay errors, cache hit rate, impression lag. If ranker fails, serve cached or chronological with privacy intact.

Security and privacy

Never trust cached candidates blindly. Recheck deletion, blocks, membership, private visibility, and regional policy before hydration. Moderation must invalidate feed caches.

Rollout and 10x

Shadow feeds, canary ranking with holdbacks, backfill after graph changes. At 10x: regional shards, pre-ranked pools, privacy invalidation fan-out.

Hard-part drill: stable ranked cursors

Suppose page one returns posts A through T. A new ranking run moves A below T before page two arrives; simply asking for scores below the last score can repeat A or skip an unseen post. Create a ranked session with a stable ordered candidate list, retain it for a bounded lifetime, and return an opaque cursor containing its session identifier and next position.

Recheck current visibility for every subsequent page and skip removed items. A model version or last post identifier alone cannot reconstruct an order if candidate membership and scores changed. If the session expires, explicitly start a fresh feed or use a documented best-effort deduplication policy. Sign or validate cursor contents so clients cannot change another user’s session.

APIs worth sketching

GET
getFeed(user_id, cursor?, limit?)

Post[] + stable cursor

POST
createPost(user_id, content, media_ids?, visibility?)

Post

POST
likePost(user_id, post_id)

Like count updated

POST
commentPost(user_id, post_id, content)

Comment

POST
followUser / unfollowUser

Graph edge changes

Core data model

users

sql
PK id
username, avatar_url, bio
followers_count, following_count
created_at

posts

sql
PK id
user_id, content
media_ids JSON
likes_count, comments_count, shares_count
visibility (public, friends)
created_at

friendships

sql
follower_id, followee_id
created_at
UQ (follower_id, followee_id)

likes

sql
user_id, post_id
created_at
UQ (user_id, post_id)

comments

sql
PK id
post_id, user_id, content
parent_id (nullable)
created_at

feed_cache

yaml
# in-memory sorted-set sketch per user timeline
key: "feed:{user_id}"
score: rank_score   # ML score or epoch ms
member: post_id
policy: keep latest ~1000 (trim tail)

High-level architecture

Clients (Web, Mobile)
↓
API Gateway
↓
Feed GenerationPost ServiceMedia ServiceNotification
↓
relational databasein-memory cachestream logCDN

Feed generation (delivery layer)

Pull (fan-out on read)

For fan-out on read, store a post once and fetch recent posts from followed authors when their follower opens the feed. If a user follows 1,000 accounts, issuing 1,000 independent reads for each refresh is expensive and exposes the page to the slowest dependency. Batch requests, cap candidates per author, and merge bounded result sets. This favors authors with large audiences who post infrequently, because inactive followers generate no delivery work.

Push (fan-out on write)

For fan-out on write, a worker inserts the new post identifier into each follower’s candidate list after the post is durably stored. Reads then begin from one prepared list rather than querying every author. The cost moves to the write path: one post with ten million followers can produce ten million inserts. Queue the work, make each insert idempotent, and expose the resulting freshness lag instead of delaying the author’s acknowledgement until every follower is updated.

Push for normal users; pull/merge for celebrities. Merge cached feed + live celebrity posts on read.

Hybrid is key for delivery: avoid writing to millions of timelines when a celebrity posts. Ranking still runs after candidates are gathered.

python
// On post creation
if author.followers_count > 10000:
  # Celebrity: no fan-out
  store in post DB only
else:
  # Normal: fan-out to followers
  for follower in get_followers(author_id):
    cacheStore.zadd(f"feed:{follower}", {post_id: score})

// On feed read
cached = cacheStore.zrevrange("feed:{user}", 0, limit)
celebrity_posts = pull_from_followed_celebrities(user_id)
merged = merge_and_rank(cached, celebrity_posts)

Feed ranking

ML ranking model

Chronological is naive. Rank by predicted engagement: P(like), P(comment), P(share), plus recency decay and content diversity.

Features

Recency expresses how quickly a post becomes less useful with age; relationship strength captures repeated meaningful interaction with its author. Content type and past clicks estimate what the reader may prefer, while engagement rate must account for how many people actually saw the post. A post with 100 likes after 200 impressions provides a different signal from 100 likes after a million impressions.

Features also have a freshness and retrieval cost. Batch-load shared author and post features, cache slowly changing features, and specify defaults for missing values. Experiment with the objective carefully: maximizing clicks alone may favor sensational content, so include user satisfaction, negative feedback, and diversity constraints.

Avoid ten consecutive posts from one author. Interleave types. Use MMR-style diversity.

Feed storage

Key feed:{user_id}, score = rank or time, member = post_id. ZREVRANGE for top N; trim to ~1000 items.

Older pages fetch from durable stores with cursor pagination, not endless cache growth.

plaintext
ZADD feed:user_123 0.95 post_456
ZADD feed:user_123 0.87 post_789
ZREVRANGE feed:user_123 0 49
ZREMRANGEBYRANK feed:user_123 0 -1001

Social graph

Adjacency list

Store follower/followee edges in SQL or a graph DB for friend-of-friend queries.

Cached follower sets

followers:{user_id} as a set for fan-out. Shard by user_id at scale.

Media processing

Store uploaded media in object storage and return a media identifier to the post service. Processing workers generate image sizes and video streaming renditions asynchronously, then publish their availability. The post can show a processing state until the minimum required rendition is ready.

Deliver the selected rendition through a CDN so scrolling through a feed does not send every byte through feed servers. Hydration should return metadata and authorized media references, not fetch large video bodies. Private media requires access control at the delivery layer as well as on the post itself.

Real-time updates

Long polling

Client asks "any new posts?" Server holds until data or timeout. Simpler than WebSocket.

WebSocket

Push when a friend posts. Low latency; scale connections regionally.

Practical pattern

Post created → stream event → notify path. Or refresh on focus with an "X new posts" banner.

Bottlenecks to call out

Celebrity fan-out

Hybrid push/pull; never full push for mega accounts.

Feed read latency

Pre-compute candidates; bound celebrity merge; cache.

Hot user feed

Replicas and CDN for static; protect origin reads.

Ranking latency

Pre-score, cache scores, batch inference, pre-rankers.

Takeaway

  • Discovery: candidates → eligibility → rank → mix → hydrate
  • Delivery: hybrid fan-out; celebrity pull/merge
  • Learning: truthful impressions; canary with holdbacks
  • Safety: ranking never overrides privacy; degrade without empty leaks
Beginner to advanced path(open guidance)

Beginner answer

Users, posts, follows, feed cache, pagination, likes, comments, and media hydration.

Advanced answer

Candidate generation, ranking, visibility, stable cursors, cache invalidation, ads blending, quotas, chronological fallback.

Deeper walkthrough

News feed deeper walkthrough

A feed is not just a list of posts. It is candidate generation, filtering, ranking, hydration, pagination, and feedback loops.

Requirement refinements

Clarify whether the feed includes only followed accounts, recommendations, ads, groups, privacy filters, and blocked users.

State freshness expectations separately from ranking quality and read latency.

API design details

Use stable cursor pagination because ranked feeds can reorder between requests.

Return tracking metadata for impressions so ranking feedback can learn from what was actually shown.

Data model and storage

Store candidate feeds separately from post content; hydrate content and visibility at read time.

Keep feedback events such as likes, hides, follows, dwell time, and reports in an analytics stream.

Scaling strategy

Use fan-out on write for normal users and pull or merge strategy for celebrity or high-rank accounts.

Cache feed candidates, but recheck post visibility and deletion during hydration.

Reliability, recovery, and operations

If ranking fails, return cached or chronological candidates instead of an empty feed.

Monitor feed p99, candidate freshness lag, ranking latency, visibility-filter misses, and fan-out backlog.

Deepening reference

News Feed: realistic production notes

Beginner mental model: A feed is candidate generation, ranking, hydration, pagination, and freshness management.

Production scope: A production feed blends follow graph content, recommended content, ads, hidden/blocked content, ranking signals, and cache invalidation.

Choose what the first version must guarantee

Start by deciding which news feed capabilities are essential. Describe the main read and write operations before choosing infrastructure. For each operation, identify the stored records it uses and the condition that makes its result correct. A fast response is useful only if it satisfies that condition.

Next, separate work the user must wait for from work that can finish later. Estimate requests per second, storage growth, and the busiest object or customer. Use those estimates to choose one bottleneck to investigate. The calculations below are example workloads with stated assumptions; they are not measured limits of a particular product.

Concrete capacity calculations

Traffic: 100M daily users, 20 feed opens/day means 2B feed reads/day, about 23K/sec average and 115K/sec at 5x peak.

Writes: 10M authors posting 2/day creates 20M posts/day, about 231/sec average. Fan-out, not raw post writes, is the real write multiplier.

Candidate cache: 1,000 candidate IDs per active user at 16 bytes each is 16 KB/user. For 50M cached users this is about 800 GB before overhead.

Ranking budget: if p99 feed latency target is 200 ms, candidate fetch, visibility filtering, ranking, hydration, and network each need explicit sub-budgets.

Invalidation: deleting one popular post from 20M cached feeds is too expensive synchronously. Mark the post unavailable and filter at hydration while async cleanup catches up.

Scaling to millions of users

Split candidate generation, ranking, and hydration into separate stages with fallbacks at each stage.

Partition home-feed caches by reader_id; partition author posts by author_id and time for profile timelines.

Use hybrid push/pull fan-out so popular authors and highly personalized feeds do not explode write amplification.

Keep visibility, blocks, and deleted-state checks close to hydration so cached candidate IDs cannot leak private content.

Use async feature pipelines for engagement signals; stale features should reduce ranking quality, not availability.

Sequence and data-flow diagram

Walk this path after the high-level design so it is clear what is synchronous, what is asynchronous, and where the system can degrade safely.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
  participant Author
  participant Post as Post service
  participant Log as Event log
  participant Fanout
  participant Cache as Feed cache
  participant Reader
  participant Ranker
  Author->>Post: Create post
  Post->>Log: Publish post event
  Log->>Fanout: Fanout candidates
  Fanout->>Cache: Add candidate IDs
  Reader->>Cache: Fetch candidates
  Reader->>Ranker: Rank and hydrate
  Ranker-->>Reader: Feed page

Core flow to walk through

1

Write post: store post, publish event, fan out to followers or mark for read-time merge.

2

Read feed: fetch candidates, filter visibility, rank, hydrate, paginate.

3

Feedback: likes, hides, follows, and dwell time update ranking features asynchronously.

Production readiness

News Feed: ship, operate, and evolve

Now examine the same request flow during failures and deployments. Tie each operational control to the user behavior it protects.

Observability

Track news feed p95/p99 latency, error rate, saturation, and user-visible freshness separately for read, write, and async paths.

Alert on leading indicators such as queue age, hot-key skew, cache miss storms, reconciliation mismatches, and error-budget burn.

Security and abuse

Enforce authentication, authorization, rate limits, input validation, audit trails, and privacy checks at the service boundary.

Treat abuse controls as a first-class path: throttling, quota isolation, moderation hooks, revocation, and safe handling of sensitive data.

Degraded modes

Name what still works if cache, search, ranking, stream processing, object storage, or a regional dependency is slow.

Prefer stale-but-safe reads, partial results, async retry, admission control, and feature disablement over corrupting core state.

Testing and rollout

Load test average, peak, and hot-object traffic; add failure-injection for retries, timeouts, backlog growth, and dependency outages.

Ship with canaries, shadow reads or dual writes when useful, rollback switches, schema compatibility checks, and replay validation.

Runbooks

Document dashboards, paging thresholds, owner actions, rollback steps, data repair commands, and customer-impact language.

Keep backfills and replays idempotent, resumable, rate-limited, and observable so repair work does not create a second incident.

10x evolution

Say which metric forces the next architecture change: shard count, cache footprint, write amplification, queue lag, p99 latency, or regional failover reserve.

Evolve one bottleneck at a time: partition by access pattern, isolate hot tenants, regionalize reads, add replayable async pipelines, then harden multi-region behavior.

Hard parts worth deepening

Do not mix candidate generation and final ranking into one vague service.

Visibility filtering must happen before returning content.

Pagination should use stable cursors, not offset.

Ads and recommendations need separate quotas and blending rules.

Failure modes and recovery

Ranking service down; return chronological or cached feed.

Fan-out backlog; feed freshness degrades but reads continue.

Deleted/private post remains in cache; hydrate path must recheck visibility.

Celebrity post overload; use hybrid fan-out.

Signals to monitor

Feed p99 measures response speed, while freshness lag measures the age of newly available content. Neither replaces the other: a cached response can be fast and stale.

Candidate count explains ranking workload, and ranking latency shows whether that workload fits the request budget. Visibility-filter misses must distinguish ordinary excluded candidates from restricted content that escaped into the delivered response.

ChoiceBest whenCost
Push modelReads dominate and follower counts are moderate.High write amplification.
Pull modelAuthors have massive followers or feed is highly personalized.Read latency and CPU.
Ranked feedEngagement and personalization matter.Explainability and freshness complexity.

Try this scenario

Describe how you remove a private post from millions of cached feeds without scanning every feed synchronously.

Primary sources

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Design candidate generation, feature hydration, ranking, experimentation, stable pagination, impression logging, feedback, and deletion/privacy filtering.

Explain the decision

How does a generic ranked news feed differ from a follower timeline?

Read the reasoning

It blends many candidate sources and model features, then needs experiment assignment, impression/click labels, diversity, and ranking-version observability.

Apply it to a production scenario

A new model lifts clicks but increases hides and latency. Should it ship?

Read one approach

Evaluate guardrail metrics, cohort/fairness effects, long-term outcomes, capacity, and rollback; CTR alone is not the product objective.

Work through the numbers

Ranking 2,000 candidates for 100,000 requests/s at 50 features each implies how many feature reads/s naively?

Read the worked calculation

Ten billion feature values/s; batch, cache, precompute, and reduce candidates before expensive ranking.

Reason through a failure

What must impression logging preserve?

Read the recovery reasoning

The exact ranking/model/experiment and visible items are attributable without double-counting, so training and product metrics remain trustworthy.