Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

System Design (High-Level Design)  Module 23 – Case studies: social, feeds and search

Design top-K trending topics

Design trending topics: count mentions in sliding windows, score growth against each topic’s baseline, merge regional top-k lists and keep bots out.

  • Advanced
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: A trend is a topic growing faster than usual, listed every minute · Mentions peak near 58,000 a second, and no read reaches a counter · Four records carry the stream: mentions, panes, baselines and lists · 4 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Estimate the mention rate, counter memory and read load of a trending service
  • Define trending as growth against a topic's own baseline, with a floor on volume
  • Count heavy hitters over sliding windows with sketches and an exact recount
  • Merge regional and partition top-k lists correctly, and filter coordinated manipulation

Before you start

At 500 million posts a day, a trending service counts about 11,574 topic mentions a second, and five times as many during a big event. Its list shows what people are suddenly talking about, not what they always talk about: every minute it finds the topics mentioned most in the last few minutes, and which of those are growing faster than usual.

The pipeline counts each topic in one-minute panes, compares each busy topic with its own baseline, discounts manipulation, and merges regional and world lists that a cache serves to everyone. Exact counts cost too much at the peak, so the counters keep sketch (data summary): A small, fixed-size summary of a stream that answers one question, such as how many distinct items it held or how often an item occurred, with a stated error instead of an exact answer. More in “HyperLogLog, Count-Min and heavy hitters”, approximate counts in fixed memory, corrected by an exact recount of a few candidates. This case study sizes the service, follows its paths, and shows why popular and trending are different lists.

A trend is a topic growing faster than usual, listed every minute

Questions first, because “trending” means different things to different products:

  • What is a topic? Hashtags and named entities extracted from posts, one mention per topic per post.
  • What is a trend? A topic mentioned far more often than its own usual rate, and often enough to be real.
  • Which lists? The top 50 trending topics for each of 100 regions and for the whole world.
  • Over what time? The last 15 minutes and the last hour, refreshed every minute.
  • How fast? A real surge should appear within two minutes of starting.
  • How exact? Approximate counts are fine; a list that is slightly out of order is fine; a list that a bot network controls is not.
Aspect Popular Trending
Measures mentions in the window the window against the topic’s usual rate
Led by the same big topics every day whatever is suddenly growing
Needs a count per topic a count, a baseline and a floor on volume

The functional requirements: count mentions by region over sliding windows, score topics by growth against their usual rate, publish regional and global top-k lists every minute, and let operators remove a topic. The non-functional ones: absorb five times the average mention rate during an event, serve the lists with a p99 (99th percentile): The latency within which 99 % of requests finish, so the slowest 1 % take longer. Tail latency is read from such high percentiles, because an average hides the slowest requests. More in “Latency, throughput and percentiles” of 50 ms, tolerate late and repeated mentions, and stay correct when one topic carries a large share of the stream.

Mentions peak near 58,000 a second, and no read reaches a counter

Quantity Assumption Result
Mentions 500 million posts a day, 2 topics each 11,574 a second on average; about 58,000 at a 5 × event peak
One minute at the peak 58,000 a second × 60 about 3.5 million mentions a minute
Exact counts for a minute 2 million distinct topics × about 50 bytes about 100 MB a minute, more in an event
A sketch for a minute 4 rows × 1,048,576 counters × 4 bytes 16.8 MB, whatever the number of topics
An hour of panes 60 one-minute sketches about 1 GB in total, across all partitions
Reads of the lists 100 million people × 3 views a day 3,472 a second, served from a cache

Counting is a stream problem and reading is a caching problem: the lists change once a minute and are the same for everyone in a region, so the 3,472 reads a second never reach the counters.

Checkpoint

Check the scope and the estimate

  1. Question 1 of 3 Weather has the most mentions in the window, as it does every day. Is it trending?
    Show the answer: Question 1 of 3

    Answer: No: it is popular, because its count is at its usual rate

    Popular is volume; trending is a count far above the topic's own usual rate, with a floor on volume. Weather leads by volume every day, so it is popular and not trending.

  2. Question 2 of 3 At the event peak the counters take about 58,000 mentions a second. How many mentions is one minute at the peak?

    Type a number.

    Show the answer: Question 2 of 3

    Answer: 3480000 mentions a minute (anything from 3132000 to 3828000 counts)

    58,000 × 60 = 3,480,000, about 3.5 million mentions, which is what one minute's pane must absorb at the peak.

  3. Question 3 of 3 Why do the counters keep a sketch of 16.8 MB a minute instead of exact counts?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: Its memory is fixed whatever the number of distinct topics, while exact counts grow with them

    A sketch keeps approximate counts in a fixed number of counters, 4 rows of 1,048,576 here, so an event that brings many new topics does not grow it. Exact counts would.

Four records carry the stream: mentions, panes, baselines and lists

  • GET /v1/trends?region=IN-MH&window=1h&limit=20 returns the current list: topic, score, the count in the window and its usual rate, and a few example post ids.
  • GET /v1/trends/global returns the merged world list.
  • POST /v1/trends/blocks lets operators remove a topic from every list.
Data Key Holds
Mention event topic the topic, region, account id, post id and event time
Window pane partition, minute one sketch of that minute’s counts, and the candidate topics
Baseline topic, region the topic’s usual mentions a minute, for each hour of the week
Published list region, window the top 50 with scores, rebuilt every minute

Panes add up by minute into any window, and lists are read whole by region.

Topics are counted once, scored every minute and served from a cache

Trending pipeline: a mention log feeds window counters, a scorer with a baseline store, an abuse filter, a merger and a trends cache.Post servicetopics extractedMention logpartitioned by topicWindow countersone-minute panes per partitionBaseline storeusual rate per topicTrend scorerthe window against the baselineAbuse filterdistinct accounts, known botsMergerregional and global top-kTrends cacheper region, every minuteTrends APIApps1. topic, region, account, time2. count3. candidates and countsusual rate4. scores5. clean lists6. publishreadread

From mentions to a trends list, refreshed every minute

Text description of the diagram

The post service extracts the topics of each post and (1) appends one mention per topic, with its region, account and time, to the mention log, which is partitioned by topic.

The window counters (2) count mentions in one-minute panes for each partition, and (3) pass the candidate topics and their counts to the trend scorer. The scorer compares each candidate's count in the window with its usual rate, read from the baseline store, and (4) passes the scores to the abuse filter, which weighs distinct accounts and removes known bots.

The merger (5) combines the clean lists into regional and global top-k lists and (6) publishes them to the trends cache, refreshed every minute. Apps read trends through the trends API, which reads only the cache.

Count. The post service writes one mention event per topic to a log partitioned by topic, so all mentions of a topic meet in one partition. Each partition keeps one-minute panes: a sketch of every topic’s count and a short list of the topics with the highest counts. Score. Every minute, the scorer sums the panes of the window for each candidate and compares the total with the topic’s usual rate for that hour, read from the baseline store; a candidate must also pass a minimum volume. Merge. The abuse filter discounts topics carried by few accounts, and the merger combines the partitions’ lists into each region’s top 50 and the global list. Serve. The lists go to a cache, and the API reads only the cache. Follow one mention through those steps:

Interactive · stage by stage One mention from a post to the trends list Step through one mention of a surging topic, from the post that carries it to the list a reader opens a minute later.
One mention from a post to the trends list, step 8 of 8. Showing Post service, Mention log, Window counters, Trend scorer (window against usual rate), Abuse filter, Merger, Trends cache. In focus: Trends cache.under 200: not listedPost servicetopics taken from a postMention logpartitioned by topicWindow countersone-minute panesTrend scorerwindow against usual rateAbuse filterdistinct accountsMergerregional and world listsTrends cacheper region, every minute

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. A post names a topic. A post about a solar storm is published. The post service extracts its topics and writes one mention event for each, with the region, the account and the time.
  2. One partition per topic. The mention log is partitioned by topic, so every mention of solar-storm, from every post server, reaches one partition and is counted in one place.
  3. Counted into its minute. The partition adds the mention to the pane of its minute. A pane holds one minute of counts, and a 5-minute window is the sum of the last five panes.
  4. Scored against its usual rate. Every minute the scorer compares each busy topic's window with the topic's usual rate. Solar-storm has 2,983 mentions where 212 were expected.
  5. Small topics are not listed. A topic needs 200 mentions in the window to be listed at all. A local quiz went from about 5 mentions to 52, eleven times its usual rate, and stays off the list.
  6. Many people, not a few accounts. The abuse filter discounts topics carried by a few accounts posting again and again. Thousands of different accounts mention solar-storm, so its score stands.
  7. Regional and world lists. The merger combines the partitions' candidates into the top 50 of each of 100 regions and into the world list.
  8. Every read from the cache. The lists go to the trends cache once a minute, the same for everyone in a region, so about 3,472 reads a second never reach the counters.

Checkpoint

Check: The four records and their keys · Count, score, merge and serve

  1. Question 1 of 2 What is the mention log partitioned by?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The topic

    Keying mentions by topic gives each topic one counter, so its count in a window is complete in one place.

  2. Question 2 of 2 A reader opens the trends list. Which part of the pipeline does the request reach?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only the trends cache, through the trends API

    Counting is a stream problem and reading is a caching problem: about 3,472 reads a second are served from the cache, and the counters only take mentions.

The program below shows why the score compares growth and not volume. For ten topics it takes the mentions in the 20 minutes before the window and in the 5-minute window itself: some topics always busy, one surging, one tiny topic that jumps. Surprise is the window’s count above the expected count, in rough standard deviations.

One program · 14 languages Popular against trending for ten topics O(n log n) to rank n topics

Python 3.14 · main.py

from math import sqrt

FLOOR = 200  # mentions a topic needs in the window to be eligible at all
TOPICS = [  # topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
    ("weather", 18015, 4504),
    ("football", 15897, 4154),
    ("elections", 13084, 3261),
    ("movies", 10397, 2774),
    ("music", 9631, 2323),
    ("recipes", 6111, 1439),
    ("solar-storm", 847, 2983),
    ("chess-final", 2461, 1609),
    ("new-phone", 3974, 1280),
    ("local-quiz", 19, 52),
]


def expected(baseline: int) -> float:
    return baseline / 20 * 5  # the window's 5 minutes at the topic's usual rate


def surprise(baseline: int, window: int) -> float:
    """Rough standard deviations above the usual rate, smoothed for small counts."""
    return (window - expected(baseline)) / sqrt(expected(baseline) + 50)


by_volume = sorted(TOPICS, key=lambda t: -t[2])
by_surprise = sorted(TOPICS, key=lambda t: -surprise(t[1], t[2]))
floored = [t for t in by_surprise if t[2] >= FLOOR]
for title, ranked in [
    ("volume", by_volume),
    ("surprise", by_surprise),
    ("floor", floored),
]:
    print(f"top 5 by {title:<9} {' '.join(t[0] for t in ranked[:5])}")
print("\ntopic        window expected surprise")
for topic, base, window in by_surprise[:5]:
    print(
        f"{topic:<12} {window:>6} {expected(base):>8.0f} {surprise(base, window):>8.1f}"
    )

In Python: sorted() with a key of -surprise sorts from the highest, and a list comprehension keeps the topics above the floor.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

const FLOOR = 200; // mentions a topic needs in the window to be eligible at all
const TOPICS = [
  // topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
  ["weather", 18015, 4504],
  ["football", 15897, 4154],
  ["elections", 13084, 3261],
  ["movies", 10397, 2774],
  ["music", 9631, 2323],
  ["recipes", 6111, 1439],
  ["solar-storm", 847, 2983],
  ["chess-final", 2461, 1609],
  ["new-phone", 3974, 1280],
  ["local-quiz", 19, 52],
];

// the window's 5 minutes at the topic's usual rate
const expected = (baseline) => (baseline / 20) * 5;

/** Rough standard deviations above the usual rate, smoothed for small counts. */
const surprise = (baseline, window) =>
  (window - expected(baseline)) / Math.sqrt(expected(baseline) + 50);

const byVolume = [...TOPICS].sort((a, b) => b[2] - a[2]);
const bySurprise = [...TOPICS].sort((a, b) => surprise(b[1], b[2]) - surprise(a[1], a[2]));
const floored = bySurprise.filter((t) => t[2] >= FLOOR);
for (const [title, ranked] of [
  ["volume", byVolume],
  ["surprise", bySurprise],
  ["floor", floored],
]) {
  console.log(
    `top 5 by ${title.padEnd(9)} ${ranked
      .slice(0, 5)
      .map((t) => t[0])
      .join(" ")}`,
  );
}
console.log("\ntopic        window expected surprise");
for (const [topic, base, window] of bySurprise.slice(0, 5)) {
  const numbers = [
    expected(base).toFixed(0).padStart(8),
    surprise(base, window).toFixed(1).padStart(8),
  ];
  console.log(`${topic.padEnd(12)} ${String(window).padStart(6)} ${numbers.join(" ")}`);
}

In JavaScript: sort takes a comparator, so b minus a sorts from the highest, and [...TOPICS] sorts a copy and keeps the original order.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

public class Main {
  static final int FLOOR = 200; // mentions a topic needs in the window to be eligible at all

  // topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
  record Topic(String name, int baseline, int window) {
    double expected() {
      return baseline / 20.0 * 5; // the window's 5 minutes at the topic's usual rate
    }

    /** Rough standard deviations above the usual rate, smoothed for small counts. */
    double surprise() {
      return (window - expected()) / Math.sqrt(expected() + 50);
    }
  }

  static String names(List<Topic> ranked) {
    return ranked.stream().limit(5).map(Topic::name).collect(Collectors.joining(" "));
  }

  public static void main(String[] args) {
    Locale.setDefault(Locale.ROOT);
    List<Topic> topics =
        List.of(
            new Topic("weather", 18015, 4504),
            new Topic("football", 15897, 4154),
            new Topic("elections", 13084, 3261),
            new Topic("movies", 10397, 2774),
            new Topic("music", 9631, 2323),
            new Topic("recipes", 6111, 1439),
            new Topic("solar-storm", 847, 2983),
            new Topic("chess-final", 2461, 1609),
            new Topic("new-phone", 3974, 1280),
            new Topic("local-quiz", 19, 52));
    List<Topic> byVolume =
        topics.stream().sorted(Comparator.comparingInt(Topic::window).reversed()).toList();
    List<Topic> bySurprise =
        topics.stream().sorted(Comparator.comparingDouble(Topic::surprise).reversed()).toList();
    List<Topic> floored = bySurprise.stream().filter(t -> t.window() >= FLOOR).toList();
    System.out.println("top 5 by volume    " + names(byVolume));
    System.out.println("top 5 by surprise  " + names(bySurprise));
    System.out.println("top 5 by floor     " + names(floored));
    System.out.println("\ntopic        window expected surprise");
    for (Topic t : bySurprise.subList(0, 5)) {
      System.out.printf(
          "%-12s %6d %8.0f %8.1f%n", t.name(), t.window(), t.expected(), t.surprise());
    }
  }
}

In Java: The record computes expected() and surprise() as methods, and Comparator.comparingDouble(Topic::surprise).reversed() sorts from the highest.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <algorithm>
#include <cmath>
#include <print>
#include <string>
#include <vector>

// mentions a topic needs in the window to be eligible at all
constexpr int FLOOR = 200;

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute
// window
struct Topic {
  std::string name;
  int baseline, window;
  // the window's 5 minutes at the topic's usual rate
  double expected() const { return baseline / 20.0 * 5; }
  // Rough standard deviations above the usual rate, smoothed for small counts.
  double surprise() const {
    return (window - expected()) / std::sqrt(expected() + 50);
  }
};

void print_top(const char *title, const std::vector<Topic> &ranked) {
  std::print("top 5 by {:<9}", title);
  for (size_t i = 0; i < 5; i++)
    std::print(" {}", ranked[i].name);
  std::println();
}

int main() {
  const std::vector<Topic> topics = {
      {"weather", 18015, 4504},   {"football", 15897, 4154},
      {"elections", 13084, 3261}, {"movies", 10397, 2774},
      {"music", 9631, 2323},      {"recipes", 6111, 1439},
      {"solar-storm", 847, 2983}, {"chess-final", 2461, 1609},
      {"new-phone", 3974, 1280},  {"local-quiz", 19, 52}};
  auto by_volume = topics, by_surprise = topics;
  std::ranges::sort(by_volume, std::greater{}, &Topic::window);
  std::ranges::sort(by_surprise, std::greater{}, &Topic::surprise);
  std::vector<Topic> floored;
  std::ranges::copy_if(by_surprise, std::back_inserter(floored),
                       [](const Topic &t) { return t.window >= FLOOR; });
  print_top("volume", by_volume);
  print_top("surprise", by_surprise);
  print_top("floor", floored);
  std::println("\ntopic        window expected surprise");
  for (size_t i = 0; i < 5; i++) {
    const auto &t = by_surprise[i];
    std::println("{:<12} {:>6} {:>8.0f} {:>8.1f}", t.name, t.window,
                 t.expected(), t.surprise());
  }
}

In C++: std::ranges::sort takes a projection, &Topic::surprise, so std::greater{} sorts from the highest, and copy_if keeps the topics above the floor.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

using static System.FormattableString;

const int Floor = 200; // mentions a topic needs in the window to be eligible at all

Topic[] topics =
[
    new("weather", 18015, 4504),
    new("football", 15897, 4154),
    new("elections", 13084, 3261),
    new("movies", 10397, 2774),
    new("music", 9631, 2323),
    new("recipes", 6111, 1439),
    new("solar-storm", 847, 2983),
    new("chess-final", 2461, 1609),
    new("new-phone", 3974, 1280),
    new("local-quiz", 19, 52),
];
var byVolume = topics.OrderByDescending(t => t.Window).ToList();
var bySurprise = topics.OrderByDescending(t => t.Surprise).ToList();
var floored = bySurprise.Where(t => t.Window >= Floor).ToList();
(string Title, List<Topic> Ranked)[] rankings =
    [("volume", byVolume), ("surprise", bySurprise), ("floor", floored)];
foreach (var (title, ranked) in rankings)
{
    string names = string.Join(" ", ranked.Take(5).Select(t => t.Name));
    Console.WriteLine($"top 5 by {title,-9} {names}");
}
Console.WriteLine("\ntopic        window expected surprise");
foreach (var t in bySurprise.Take(5))
{
    Console.WriteLine(Invariant($"{t.Name,-12} {t.Window,6} {t.Expected,8:F0} {t.Surprise,8:F1}"));
}

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
record Topic(string Name, int Baseline, int Window)
{
    // the window's 5 minutes at the topic's usual rate
    public double Expected => Baseline / 20.0 * 5;

    // Rough standard deviations above the usual rate, smoothed for small counts.
    public double Surprise => (Window - Expected) / Math.Sqrt(Expected + 50);
}

In C#: Expected and Surprise are computed properties of the record, OrderByDescending sorts by them, and Invariant formats the numbers the same way in every culture.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

// FLOOR: mentions a topic needs in the window to be eligible at all
enum { FLOOR = 200, N = 10 };

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute
// window
typedef struct {
  const char *name;
  int baseline, window;
} Topic;

// the window's 5 minutes at the topic's usual rate
static double expected(const Topic *t) { return t->baseline / 20.0 * 5; }

// Rough standard deviations above the usual rate, smoothed for small counts.
static double surprise(const Topic *t) {
  return (t->window - expected(t)) / sqrt(expected(t) + 50);
}

static int by_volume(const void *x, const void *y) {
  return ((const Topic *)y)->window - ((const Topic *)x)->window;
}

static int by_surprise(const void *x, const void *y) {
  const double a = surprise(x), b = surprise(y);
  return (a < b) - (a > b);
}

int main(void) {
  Topic t[N] = {{"weather", 18015, 4504},   {"football", 15897, 4154},
                {"elections", 13084, 3261}, {"movies", 10397, 2774},
                {"music", 9631, 2323},      {"recipes", 6111, 1439},
                {"solar-storm", 847, 2983}, {"chess-final", 2461, 1609},
                {"new-phone", 3974, 1280},  {"local-quiz", 19, 52}};
  qsort(t, N, sizeof t[0], by_volume);
  printf("top 5 by volume   ");
  for (int i = 0; i < 5; i++)
    printf(" %s", t[i].name);
  qsort(t, N, sizeof t[0], by_surprise);
  printf("\ntop 5 by surprise ");
  for (int i = 0; i < 5; i++)
    printf(" %s", t[i].name);
  printf("\ntop 5 by floor    ");
  for (int i = 0, shown = 0; i < N && shown < 5; i++)
    if (t[i].window >= FLOOR) {
      printf(" %s", t[i].name);
      shown++;
    }
  printf("\n\ntopic        window expected surprise\n");
  for (int i = 0; i < 5; i++)
    printf("%-12s %6d %8.0f %8.1f\n", t[i].name, t[i].window, expected(&t[i]),
           surprise(&t[i]));
  return 0;
}

In C: qsort sorts in place, so the volume ranking is printed before the array is sorted again by surprise; the comparator turns two doubles into -1, 0 or 1.

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

const FLOOR = 200; // mentions a topic needs in the window to be eligible at all
// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
const TOPICS = [
    ['weather', 18015, 4504],
    ['football', 15897, 4154],
    ['elections', 13084, 3261],
    ['movies', 10397, 2774],
    ['music', 9631, 2323],
    ['recipes', 6111, 1439],
    ['solar-storm', 847, 2983],
    ['chess-final', 2461, 1609],
    ['new-phone', 3974, 1280],
    ['local-quiz', 19, 52],
];

function expected(int $baseline): float
{
    return $baseline / 20 * 5; // the window's 5 minutes at the topic's usual rate
}

/** Rough standard deviations above the usual rate, smoothed for small counts. */
function surprise(int $baseline, int $window): float
{
    return ($window - expected($baseline)) / sqrt(expected($baseline) + 50);
}

$byVolume = TOPICS;
usort($byVolume, fn(array $a, array $b): int => $b[2] <=> $a[2]);
$bySurprise = TOPICS;
$scoreOf = fn(array $t): float => surprise($t[1], $t[2]);
usort($bySurprise, fn(array $a, array $b): int => $scoreOf($b) <=> $scoreOf($a));
$floored = array_values(array_filter($bySurprise, fn(array $t): bool => $t[2] >= FLOOR));
$rankings = ['volume' => $byVolume, 'surprise' => $bySurprise, 'floor' => $floored];
foreach ($rankings as $title => $ranked) {
    printf("top 5 by %-9s %s\n", $title, implode(' ', array_column(array_slice($ranked, 0, 5), 0)));
}
echo "\ntopic        window expected surprise\n";
foreach (array_slice($bySurprise, 0, 5) as [$topic, $base, $window]) {
    printf("%-12s %6d %8.0f %8.1f\n", $topic, $window, expected($base), surprise($base, $window));
}

In PHP: usort sorts a copy of the constant array, because arrays are values, and <=> compares two surprises from the highest when its operands are swapped.

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

const FLOOR: u32 = 200; // mentions a topic needs in the window to be eligible at all

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
const TOPICS: [(&str, u32, u32); 10] = [
    ("weather", 18015, 4504),
    ("football", 15897, 4154),
    ("elections", 13084, 3261),
    ("movies", 10397, 2774),
    ("music", 9631, 2323),
    ("recipes", 6111, 1439),
    ("solar-storm", 847, 2983),
    ("chess-final", 2461, 1609),
    ("new-phone", 3974, 1280),
    ("local-quiz", 19, 52),
];

/// The window's 5 minutes at the topic's usual rate.
fn expected(baseline: u32) -> f64 {
    f64::from(baseline) / 20.0 * 5.0
}

/// Rough standard deviations above the usual rate, smoothed for small counts.
fn surprise(baseline: u32, window: u32) -> f64 {
    (f64::from(window) - expected(baseline)) / (expected(baseline) + 50.0).sqrt()
}

fn main() {
    let mut by_volume = TOPICS.to_vec();
    by_volume.sort_by_key(|t| std::cmp::Reverse(t.2));
    let mut by_surprise = TOPICS.to_vec();
    by_surprise.sort_by(|a, b| surprise(b.1, b.2).total_cmp(&surprise(a.1, a.2)));
    let floored: Vec<_> = by_surprise
        .iter()
        .copied()
        .filter(|t| t.2 >= FLOOR)
        .collect();
    for (title, ranked) in [
        ("volume", &by_volume),
        ("surprise", &by_surprise),
        ("floor", &floored),
    ] {
        let names: Vec<&str> = ranked.iter().take(5).map(|t| t.0).collect();
        println!("top 5 by {title:<9} {}", names.join(" "));
    }
    println!("\ntopic        window expected surprise");
    for &(topic, base, window) in &by_surprise[..5] {
        let (e, s) = (expected(base), surprise(base, window));
        println!("{topic:<12} {window:>6} {e:>8.0} {s:>8.1}");
    }
}

In Rust: f64 has no total order of its own, so total_cmp sorts the surprises, and std::cmp::Reverse sorts the volumes from the highest.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

type Topic = [name: string, baseline: number, window: number];

const FLOOR = 200; // mentions a topic needs in the window to be eligible at all
const TOPICS: Topic[] = [
  // topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
  ["weather", 18015, 4504],
  ["football", 15897, 4154],
  ["elections", 13084, 3261],
  ["movies", 10397, 2774],
  ["music", 9631, 2323],
  ["recipes", 6111, 1439],
  ["solar-storm", 847, 2983],
  ["chess-final", 2461, 1609],
  ["new-phone", 3974, 1280],
  ["local-quiz", 19, 52],
];

// the window's 5 minutes at the topic's usual rate
const expected = (baseline: number): number => (baseline / 20) * 5;

/** Rough standard deviations above the usual rate, smoothed for small counts. */
const surprise = (baseline: number, window: number): number =>
  (window - expected(baseline)) / Math.sqrt(expected(baseline) + 50);

const byVolume = [...TOPICS].sort((a, b) => b[2] - a[2]);
const bySurprise = [...TOPICS].sort((a, b) => surprise(b[1], b[2]) - surprise(a[1], a[2]));
const floored = bySurprise.filter((t) => t[2] >= FLOOR);
const rankings: [string, Topic[]][] = [
  ["volume", byVolume],
  ["surprise", bySurprise],
  ["floor", floored],
];
for (const [title, ranked] of rankings) {
  console.log(
    `top 5 by ${title.padEnd(9)} ${ranked
      .slice(0, 5)
      .map((t) => t[0])
      .join(" ")}`,
  );
}
console.log("\ntopic        window expected surprise");
for (const [topic, base, window] of bySurprise.slice(0, 5)) {
  const numbers = [
    expected(base).toFixed(0).padStart(8),
    surprise(base, window).toFixed(1).padStart(8),
  ];
  console.log(`${topic.padEnd(12)} ${String(window).padStart(6)} ${numbers.join(" ")}`);
}

In TypeScript: The labelled tuple type Topic names the three fields, and rankings is typed as pairs of a title and a ranked list.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"cmp"
	"fmt"
	"math"
	"slices"
	"strings"
)

const floor = 200 // mentions a topic needs in the window to be eligible at all

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
type topic struct {
	name             string
	baseline, window int
}

// expected is the window's 5 minutes at the topic's usual rate.
func (t topic) expected() float64 { return float64(t.baseline) / 20 * 5 }

// surprise is roughly how many standard deviations above the usual rate, smoothed for small counts.
func (t topic) surprise() float64 {
	return (float64(t.window) - t.expected()) / math.Sqrt(t.expected()+50)
}

func top5(ranked []topic) string {
	names := []string{}
	for _, t := range ranked[:5] {
		names = append(names, t.name)
	}
	return strings.Join(names, " ")
}

func main() {
	topics := []topic{
		{"weather", 18015, 4504}, {"football", 15897, 4154}, {"elections", 13084, 3261},
		{"movies", 10397, 2774}, {"music", 9631, 2323}, {"recipes", 6111, 1439},
		{"solar-storm", 847, 2983}, {"chess-final", 2461, 1609}, {"new-phone", 3974, 1280},
		{"local-quiz", 19, 52},
	}
	byVolume, bySurprise := slices.Clone(topics), slices.Clone(topics)
	slices.SortFunc(byVolume, func(a, b topic) int { return cmp.Compare(b.window, a.window) })
	slices.SortFunc(bySurprise, func(a, b topic) int {
		return cmp.Compare(b.surprise(), a.surprise())
	})
	var floored []topic
	for _, t := range bySurprise {
		if t.window >= floor {
			floored = append(floored, t)
		}
	}
	fmt.Println("top 5 by volume    " + top5(byVolume))
	fmt.Println("top 5 by surprise  " + top5(bySurprise))
	fmt.Println("top 5 by floor     " + top5(floored))
	fmt.Println("\ntopic        window expected surprise")
	for _, t := range bySurprise[:5] {
		fmt.Printf("%-12s %6d %8.0f %8.1f\n", t.name, t.window, t.expected(), t.surprise())
	}
}

In Go: expected and surprise are methods of the topic struct, and cmp.Compare with its arguments swapped sorts from the highest.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

let floor = 200  // mentions a topic needs in the window to be eligible at all

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
struct Topic {
  let name: String
  let baseline: Int
  let window: Int

  /// The window's 5 minutes at the topic's usual rate.
  var expected: Double { Double(baseline) / 20 * 5 }

  /// Rough standard deviations above the usual rate, smoothed for small counts.
  var surprise: Double { (Double(window) - expected) / (expected + 50).squareRoot() }
}

let topics = [
  ("weather", 18015, 4504), ("football", 15897, 4154), ("elections", 13084, 3261),
  ("movies", 10397, 2774), ("music", 9631, 2323), ("recipes", 6111, 1439),
  ("solar-storm", 847, 2983), ("chess-final", 2461, 1609), ("new-phone", 3974, 1280),
  ("local-quiz", 19, 52),
].map { Topic(name: $0.0, baseline: $0.1, window: $0.2) }

func pad(_ text: String, _ width: Int, left: Bool = false) -> String {
  let spaces = String(repeating: " ", count: max(0, width - text.count))
  return left ? text + spaces : spaces + text
}

/// x rounded to one digit after the point
func oneDecimal(_ x: Double) -> String {
  let n = Int((x * 10).rounded())
  return "\(n / 10).\(n % 10)"
}

let byVolume = topics.sorted { $0.window > $1.window }
let bySurprise = topics.sorted { $0.surprise > $1.surprise }
let floored = bySurprise.filter { $0.window >= floor }
for (title, ranked) in [("volume", byVolume), ("surprise", bySurprise), ("floor", floored)] {
  print(
    "top 5 by " + pad(title, 9, left: true) + " "
      + ranked.prefix(5).map(\.name).joined(separator: " "))
}
print("\ntopic        window expected surprise")
for t in bySurprise.prefix(5) {
  let numbers = pad(String(t.window), 6) + pad(String(Int(t.expected.rounded())), 9)
  print(pad(t.name, 12, left: true) + " " + numbers + pad(oneDecimal(t.surprise), 9))
}

In Swift: expected and surprise are computed properties of the struct; without Foundation the standard library has no number format, so oneDecimal and pad lay out the table.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

FLOOR = 200 # mentions a topic needs in the window to be eligible at all

# topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
Topic = Data.define(:name, :baseline, :window) do
  # the window's 5 minutes at the topic's usual rate
  def expected = baseline / 20.0 * 5

  # Rough standard deviations above the usual rate, smoothed for small counts.
  def surprise = (window - expected) / Math.sqrt(expected + 50)
end

TOPICS = [
  ['weather', 18_015, 4504], ['football', 15_897, 4154], ['elections', 13_084, 3261],
  ['movies', 10_397, 2774], ['music', 9631, 2323], ['recipes', 6111, 1439],
  ['solar-storm', 847, 2983], ['chess-final', 2461, 1609], ['new-phone', 3974, 1280],
  ['local-quiz', 19, 52]
].map { |name, baseline, window| Topic.new(name, baseline, window) }.freeze

by_surprise = TOPICS.sort_by { |t| -t.surprise }
rankings = { 'volume' => TOPICS.sort_by { |t| -t.window }, 'surprise' => by_surprise,
             'floor' => by_surprise.select { |t| t.window >= FLOOR } }
rankings.each do |title, ranked|
  puts "top 5 by #{title.ljust(9)} #{ranked.first(5).map(&:name).join(' ')}"
end
puts "\ntopic        window expected surprise"
by_surprise.first(5).each do |t|
  numbers = "#{t.window.to_s.rjust(6)} #{format('%.0f', t.expected).rjust(8)}"
  puts "#{t.name.ljust(12)} #{numbers} #{format('%.1f', t.surprise).rjust(8)}"
end

In Ruby: Data.define gives each topic its fields and its two methods, and sort_by with a negated surprise sorts from the highest.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

import kotlin.math.roundToLong
import kotlin.math.sqrt

const val FLOOR = 200 // mentions a topic needs in the window to be eligible at all

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
data class Topic(
    val name: String,
    val baseline: Int,
    val window: Int,
) {
    val expected get() = baseline / 20.0 * 5 // the window's 5 minutes at the topic's usual rate

    /** Rough standard deviations above the usual rate, smoothed for small counts. */
    val surprise get() = (window - expected) / sqrt(expected + 50)
}

// the common library has no number format: one digit after the point
fun Double.oneDecimal(): String {
    val n = (this * 10).roundToLong()
    return "${n / 10}.${n % 10}"
}

fun main() {
    val topics =
        listOf(
            Topic("weather", 18015, 4504),
            Topic("football", 15897, 4154),
            Topic("elections", 13084, 3261),
            Topic("movies", 10397, 2774),
            Topic("music", 9631, 2323),
            Topic("recipes", 6111, 1439),
            Topic("solar-storm", 847, 2983),
            Topic("chess-final", 2461, 1609),
            Topic("new-phone", 3974, 1280),
            Topic("local-quiz", 19, 52),
        )
    val bySurprise = topics.sortedByDescending { it.surprise }
    val rankings =
        listOf(
            "volume" to topics.sortedByDescending { it.window },
            "surprise" to bySurprise,
            "floor" to bySurprise.filter { it.window >= FLOOR },
        )
    for ((title, ranked) in rankings) {
        println("top 5 by ${title.padEnd(9)} " + ranked.take(5).joinToString(" ") { it.name })
    }
    println("\ntopic        window expected surprise")
    for (t in bySurprise.take(5)) {
        val numbers =
            t.window.toString().padStart(6) +
                t.expected
                    .roundToLong()
                    .toString()
                    .padStart(9)
        println(t.name.padEnd(12) + " " + numbers + t.surprise.oneDecimal().padStart(9))
    }
}

In Kotlin: expected and surprise are properties of the data class, sortedByDescending sorts a copy, and oneDecimal formats a number, which the common library cannot.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

import 'dart:math';

const floor = 200; // mentions a topic needs in the window to be eligible at all

// topic, mentions in the 20 minutes before the window, mentions in the 5-minute window
const topics = [
  ('weather', 18015, 4504),
  ('football', 15897, 4154),
  ('elections', 13084, 3261),
  ('movies', 10397, 2774),
  ('music', 9631, 2323),
  ('recipes', 6111, 1439),
  ('solar-storm', 847, 2983),
  ('chess-final', 2461, 1609),
  ('new-phone', 3974, 1280),
  ('local-quiz', 19, 52),
];

/// The window's 5 minutes at the topic's usual rate.
double expected(int baseline) => baseline / 20 * 5;

/// Rough standard deviations above the usual rate, smoothed for small counts.
double surprise(int baseline, int window) =>
    (window - expected(baseline)) / sqrt(expected(baseline) + 50);

void main() {
  final byVolume = [...topics]..sort((a, b) => b.$3.compareTo(a.$3));
  final bySurprise = [...topics]
    ..sort((a, b) => surprise(b.$2, b.$3).compareTo(surprise(a.$2, a.$3)));
  final floored = bySurprise.where((t) => t.$3 >= floor).toList();
  for (final (title, ranked) in [
    ('volume', byVolume),
    ('surprise', bySurprise),
    ('floor', floored),
  ]) {
    print(
      'top 5 by ${title.padRight(9)} ${ranked.take(5).map((t) => t.$1).join(' ')}',
    );
  }
  print('\ntopic        window expected surprise');
  for (final (topic, base, window) in bySurprise.take(5)) {
    final numbers =
        '${'$window'.padLeft(6)} ${expected(base).toStringAsFixed(0).padLeft(8)}';
    print(
      '${topic.padRight(12)} $numbers ${surprise(base, window).toStringAsFixed(1).padLeft(8)}',
    );
  }
}

In Dart: Each topic is a record, read as t.$1 to t.$3, and the cascade ..sort sorts a copy of the constant list in place.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

top 5 by volume    weather football elections solar-storm movies
top 5 by surprise  solar-storm chess-final new-phone local-quiz movies
top 5 by floor     solar-storm chess-final new-phone movies football

topic        window expected surprise
solar-storm    2983      212    171.3
chess-final    1609      615     38.5
new-phone      1280      994      8.9
local-quiz       52        5      6.4
movies         2774     2599      3.4

By volume, the last 5 minutes belong to weather, football and elections, which lead every day. By surprise, solar-storm leads with 2,983 mentions against 212 expected, followed by chess-final at 2.6 times its usual rate. The tiny topic grew about elevenfold, from an expected 4.75 to 52, and would rank fourth without the floor of 200 mentions: growth alone rewards noise.

Key idea

Popular is volume; trending is volume far above a topic’s own usual rate, with a floor that keeps tiny topics out.

Checkpoint

Check: Surprise with a floor

  1. Question 1 of 2 In the program, which topic leads by surprise, and why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Solar-storm, with 2,983 mentions in the window against 212 expected

    Surprise measures how far the window is above the expected count, so a topic that is usually quiet and is suddenly busy leads the list.

  2. Question 2 of 2 Without the floor of 200 mentions, where would local-quiz, which went from about 5 mentions to 52, rank by surprise?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Fourth, ahead of movies with 2,774 mentions

    Growth alone rewards noise: a tiny topic can jump tenfold by chance. The floor lists only the topics with enough volume to be real.

Practise: score topics against their baseline

Write the score and the floor that a trend scorer applies every minute, and run the sample tests:

Then practise, weakest idea first:

Practice that adapts to you

Practise this lesson

12 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 500 million posts a day mention 2 topics each on average. How many mentions a second is that on average?

    Type a number.

    A hint

    A day has 86,400 seconds.

    Show the answer: Question 1 of 12

    Answer: 11574 mentions a second (anything from 10417 to 12731 counts)

    500,000,000 × 2 = 1 billion mentions a day, and 1,000,000,000 ÷ 86,400 ≈ 11,574 a second. Trends exist for the moments when this is far from average.

  2. Question 2 of 12 A big event brings five times the average rate. How many mentions a second must the counters absorb?

    Type a number.

    Show the answer: Question 2 of 12

    Answer: 57870 mentions a second (anything from 52083 to 63657 counts)

    11,574 × 5 ≈ 57,870 mentions a second. Size the counters for this peak, and expect much of it to land on one topic, which is a deep dive of its own.

  3. Question 3 of 12 A Count-Min sketch has 4 rows of 1,048,576 counters of 4 bytes. How many megabytes is one sketch?

    Type a number.

    Show the answer: Question 3 of 12

    Answer: 16.8 MB (anything from 15.1 to 18.5 counts)

    4 × 1,048,576 × 4 bytes = 16,777,216 bytes, about 16.8 MB (16 MiB), whatever the number of distinct topics. An exact map of 2 million topics at about 50 bytes an entry would need about 100 MB for the same minute.

  4. Question 4 of 12 100 million people look at the trends list 3 times a day. How many reads a second is that on average?

    Type a number.

    Show the answer: Question 4 of 12

    Answer: 3472 reads a second (anything from 3125 to 3819 counts)

    100,000,000 × 3 ÷ 86,400 ≈ 3,472 reads a second. The list changes once a minute and is the same for everyone in a region, so a cache serves all of them and the counters never see a read.

  5. Question 5 of 12 Which signals make a topic trending rather than merely popular?
    Show the answer: Question 5 of 12

    Answer:

    • Its count in the window is far above its own usual rate
    • Its count in the window is above a minimum volume
    • Many distinct accounts mention it, not a few accounts many times

    Trending means unusual growth, measured against the topic's own baseline, with enough volume to be real and enough distinct accounts to be organic. A large all-time total describes a popular topic, and a single active account is noise or manipulation.

  6. Question 6 of 12 Which lists does the trending service publish?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: The top 50 trending topics for each of 100 regions and for the whole world, refreshed every minute

    Regional and world top-50 lists, refreshed every minute, are what make one cached copy per region enough for every reader.

  7. Question 7 of 12 Why are window panes keyed by partition and minute?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: So that any window is the sum of the panes of its minutes

    Counting each minute once and adding panes forms every window without counting a mention twice.

  8. Question 8 of 12 What is a published list keyed by?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: The region and the window

    A list for each region and window, rebuilt every minute, is what the API reads from the cache.

  9. Question 9 of 12 Where does the trends API read the lists from?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: The trends cache

    Reads end at the cache, so a surge of readers during an event never slows the counting.

  10. Question 10 of 12 In which order does a mention pass through the pipeline?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Mention log, window counters, trend scorer, abuse filter, merger, trends cache

    Each stage needs the one before it: counts for scores, scores for the abuse filter, clean lists for the merger, and merged lists for the cache.

  11. Question 11 of 12 Which topics lead the program's last 5 minutes by volume?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Weather, football and elections

    The volume list is the popular list. The surprise list, with its floor, is the trending one.

  12. Question 12 of 12 Chess-final had 1,609 mentions in the window against 615 expected. How many times its usual rate is that?

    Type a number.

    Show the answer: Question 12 of 12

    Answer: 2.6 times its usual rate (anything from 2.45 to 2.75 counts)

    1,609 ÷ 615 ≈ 2.6. Its surprise, 38.5, is far smaller than solar-storm's, which is more than 14 times its usual rate.

The deep dives this design must answer

A round then probes these questions:

  • Counting in fixed memory: a Count-Min sketch keeps approximate counts for every topic in a few rows of counters.
  • Finding the heavy hitters: a heap of candidates keeps the topics with the highest estimates.
  • Correcting the sketch: an exact recount of the candidates removes the sketch’s overestimates.
  • Sliding windows: one-minute panes are added together to form any window.
  • Late events: a watermark decides when a window is complete enough to publish.
  • Merging lists: partitions that split a topic’s mentions must each send more than k candidates.
  • Hot topics: pre-aggregation at the producers keeps one topic from swamping its partition.
  • Manipulation: HyperLogLog counts distinct accounts per topic, so a few accounts cannot fake a trend.

Each of them changes a number in the estimates, which is what an interviewer listens for.

Counting heavy hitters in windows, measured Pro: opens with a Pro, Premium or Ultimate pass

The counters decide both the cost and the accuracy of the service. This part runs a sketch with a candidate list against exact counts, checks the result against the sketch’s error bound, and builds sliding windows that tolerate late events.

In this part:

  • A sketch picks the candidates and an exact recount ranks them
  • The Count-Min error, with numbers
  • Panes form every window, and late mentions correct them

1 diagram · 1 runnable example (Python) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Design a news feed

Merging top-k lists and splitting hot topics, tested Pro: opens with a Pro, Premium or Ultimate pass

A list computed in many places must be merged, and the merge is only as good as what each place sends. This part shows a merge that is exact whatever the data, tests three merges against the exact answer at scale, and then handles the topic that swamps its partition.

In this part:

  • Three rounds find the exact top K from partial counts
  • Three ways to merge, tested against the exact answer
  • A hot topic swamps its partition, with numbers
  • Baselines by hour of the week keep weekly rhythms from trending

1 diagram · 2 runnable examples (JavaScript) · about 18 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Manipulation, failure drills, bottlenecks and the rubric Pro: opens with a Pro, Premium or Ultimate pass

A trending list is a target, so the last part defends it, drills its failures, scales it and turns it into a scored interview answer.

In this part:

  • Weigh distinct accounts, not mentions
  • Each failure has a fallback that keeps the list honest
  • Bottlenecks at 10× and 100×
  • What each level is expected to cover
  • A 45-minute self-mock

about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

Choose your opening move for each brief:

Checkpoint

Your first decision

  1. Question 1 of 2 Brief: in a cup final one topic carries 40 % of all mentions, and the partition that counts it falls minutes behind. What is your first decision?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Pre-aggregate at the producers, so each post server sends one count a second for the topic

    Partitioning by topic puts one topic on one partition, so the fix is fewer events for it: each producer adds up its mentions and sends a count.

  2. Question 2 of 2 Brief: 5,000 new accounts post the same hashtag every few seconds, and it is about to top the list. What is your first decision?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Weigh topics by distinct accounts, not by raw mentions

    The abuse filter discounts topics carried by few accounts. Counting distinct accounts per topic makes a campaign look as small as it is.

  1. Mid to senior

    During a cup final, one topic receives 40 % of all mentions, and the partition that counts it falls minutes behind while the others are idle. Fix the counting without losing or double-counting mentions.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    64 regional servers each send their top 10 topics to a global merger every minute. Explain why the global top 10 can be wrong, and design a merge that is exact.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    5,000 accounts created last week each post the same hashtag every few seconds, and it reaches the top of the trends. Keep such campaigns out without hiding a real surge such as breaking news.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Trending means growth against a topic’s own baseline, with a floor on volume; popular means volume.
  • Partition the mention log by topic so that each topic is counted in one place, and serve the lists from a cache.
  • Count in one-minute panes and add panes to form each window.
  • Size the counters for an event peak, about five times the average.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.