9 min read

The Billion-User Latency Wall: How Pinterest Rebuilt Its Ad Serving Core Around a Novel Tiered Columnar Store

Pinterest: Scaling Ad Serving with Tiered Columnar Storage

The Billion-User Latency Wall: How Pinterest Rebuilt Its Ad Serving Core Around a Novel Tiered Columnar Store

Imagine you’re scrolling through Pinterest, looking for mid-century modern living room inspiration. You tap a Pin of a walnut sideboard. In the 200 milliseconds it takes for the next screen to load, a silent, titanic struggle has occurred behind the scenes.

Pinterest’s ad-serving engine has just scanned hundreds of millions of candidate ads, cross-referenced them with your unique user profile—composed of thousands of real-time signals—and calculated a relevance score, all while ensuring your privacy remains cryptographically secure.

For years, the industry standard for privacy-safe ad serving was k-anonymity: a model where users are hidden in a crowd of k individuals to prevent re-identification. But as our feature sets grew into the thousands and our latency requirements plummeted into the sub-millisecond range, k-anonymity became a bottleneck. It was too coarse for the precision we needed, and our legacy Key-Value (KV) stores were buckling under the weight of “wide” user profiles.

This is the story of how Pinterest Engineering moved beyond k-anonymity, ditching traditional row-based lookups for a Novel Tiered Columnar Store designed from the ground up for the era of Differential Privacy and microsecond-scale infrastructure.


The Crisis of the “Wide” User Profile

At Pinterest scale, a “user profile” isn’t just a row in a database with a name and an email. It is a massive, sparse vector of features:

  • Historical interactions: The last 500 Pins you clicked.
  • Real-time signals: The search query you entered three seconds ago.
  • Embeddings: 256-dimensional vectors representing your aesthetic preferences.
  • Contextual data: Your device type, region, and language.

The KV Store Wall

Historically, we stored these profiles in a distributed Key-Value store (think RocksDB-backed clusters). When an ad request came in, we fetched the entire user profile by user_id.

This worked when profiles were small. But as our ML models evolved, profiles ballooned. We faced a classic Read Amplification problem: To score a “Home Decor” ad, the system only needed 5% of the user’s features (those related to furniture). However, because we used a row-based KV store, we had to fetch the entire 100KB profile every single time.

At 10 million queries per second (QPS), this was unsustainable. Our P99 latencies were spiking, and our network bandwidth costs were astronomical. We were spending 90% of our compute time deserializing data we didn’t even use.


Beyond k-Anonymity: The Privacy Pivot

While we struggled with performance, the privacy landscape was shifting. The old method of k-anonymity—grouping users into buckets of 1,000 to mask identity—was becoming insufficient. Modern privacy-preserving machine learning requires Differential Privacy (DP), where noise is mathematically injected into the data to ensure that no individual’s data can be reverse-engineered.

Implementing DP on top of a row-based KV store is a nightmare. To inject noise correctly, you need to aggregate statistics across columns of data. If your data is stored in rows, you have to scan the entire database to perform a single DP update.

We realized we needed a storage engine that was:

  1. Columnar: So we could fetch only the features needed for a specific ad.
  2. Tiered: To keep “hot” real-time features in memory and “cold” historical features on NVMe.
  3. Privacy-Native: Built to support differentially private noise injection at the storage layer.

The Architecture: A Novel Tiered Columnar Store

We didn’t just want a new database; we wanted a specialized engine for the ad-serving path. We call it Pin-ColStore.

1. The Columnar Layout

In Pin-ColStore, data is not grouped by user_id. Instead, it is partitioned by FeatureID.

Each feature (e.g., last_clicked_category) is stored in its own contiguous block of memory or disk. When a “Home Decor” ad needs to be scored, the engine performs a “gather” operation, pulling only the relevant feature columns.

Why this matters for latency: By switching to columnar storage, we reduced our per-request data transfer by 85%. We went from fetching a massive “blob” to pinpointing exactly the bytes needed for the ML model.

2. Tiered Storage Strategy (L1/L2/L3)

Not all user data is created equal. The fact that you clicked a “Blue Sofa” 10 seconds ago is infinitely more valuable for ad serving than the fact that you pinned a “Wedding Cake” three years ago.

Pin-ColStore implements a three-tier hierarchy:

  • L1 (Hot - RAM): Real-time features from the last 30 minutes. We use a custom Lock-Free Hash Map to ensure sub-10-microsecond lookups.
  • L2 (Warm - Local NVMe): Features from the last 30 days. We use SPDK (Storage Performance Development Kit) to bypass the Linux kernel and talk directly to the SSDs, avoiding context-switch overhead.
  • L3 (Cold - S3/Remote): Long-term historical trends. This data is compressed using Zstandard (Zstd) and pulled asynchronously.

3. The “Virtual Row” Index

The biggest challenge with columnar stores is “stitching” the data back together. If Feature A is in RAM and Feature B is on NVMe, how do we align them for user_123?

We developed a Compressed Bitmap Index (based on Roaring Bitmaps). Every user is assigned a 64-bit internal ID. The index maps these IDs to offsets within the column files. This allows us to perform vectorized lookups—fetching features for a batch of 500 candidate ads in a single CPU instruction (SIMD).


Deep Dive: The Sub-Millisecond “Gather” Engine

The “Gather” phase is where the magic happens. When the Ad Scorer requests features, it sends a list of (user_id, feature_list).

Vectorized Execution and SIMD

Instead of looping through users one by one, our engine uses AVX-512 instructions. It loads a batch of user offsets into a CPU register and “gathers” the values from the columnar segments in parallel.

// Simplified conceptual look at the vectorized gather
void gather_features(const uint64_t* offsets, const float* column_data, float* output, int batch_size) {
    #pragma omp simd
    for (int i = 0; i < batch_size; i++) {
        output[i] = column_data[offsets[i]];
    }
}

This approach allows us to process user profiles at the speed of the memory bus, rather than being limited by CPU instructions.

Kernel Bypass with io_uring

To achieve sub-millisecond lookups on Tier 2 (NVMe), we couldn’t afford the latency of standard read() system calls. Each syscall involves a transition from user-space to kernel-space, which takes several microseconds.

We implemented io_uring, a modern Linux interface for asynchronous I/O. By submitting a batch of read requests to a ring buffer, the kernel can process them without interrupting the application. This dropped our P99 NVMe access time from 1.2ms to 400μs.


Solving the Privacy Puzzle: Differential Privacy at Scale

The shift from k-anonymity to Differential Privacy (DP) was a core requirement. In our new columnar store, we implement Storage-Level Noise Injection.

When an analyst or an ML trainer queries a column of features, the engine doesn’t return the raw value. Instead, it applies a Laplacian Noise Function directly to the columnar data before it leaves the storage node.

Because the data is columnar, we can pre-calculate the “Sensitivity” of each column (how much one user can change the total). This makes DP mathematically sound and computationally efficient. We are no longer hiding users in a crowd (k-anonymity); we are mathematically ensuring that no single user’s data can be distinguished from the noise—at microsecond speeds.


The Engineering “Aha!” Moment: Bit-Packing Embeddings

One of the most interesting engineering curiosities we encountered was the storage of User Embeddings. These are 256-dimension vectors of floating-point numbers.

Storing these as raw floats (4 bytes each) was killing our cache hit rate. We discovered that for the purpose of ad ranking, we didn’t need 32-bit precision. We implemented a Product Quantization (PQ) scheme where we compressed these vectors into 8-bit codes.

By bit-packing these codes into our columnar store, we shrunk the embedding size by 75%. This allowed us to fit nearly all “Warm” embeddings into the L1 (RAM) tier, which resulted in a 40% boost in ad relevance because we could now afford to use more complex vectors in our real-time scoring.


Results: By the Numbers

The transition from a row-based KV store to our Tiered Columnar Store was a multi-quarter effort, but the results redefined what we thought was possible:

  • P99 Latency: Dropped from 15ms (KV store) to 0.8ms (Pin-ColStore).
  • Throughput: We can now handle 15M+ QPS on the same hardware footprint.
  • Infrastructure Cost: Reduced by 45% due to lower RAM requirements (thanks to NVMe tiering and bit-packing).
  • Feature Freshness: Real-time signals are now available for ad serving in under 1 second, down from minutes.

Lessons from the Trenches

Rebuilding the heart of an ad-serving engine while it’s processing millions of dollars in revenue is like changing the engines on a Boeing 747 while it’s mid-flight. Here are our key takeaways for other engineering teams:

1. Rows are for Writing, Columns are for Reading

If your workload involves fetching specific attributes from a wide entity, stop trying to make Key-Value stores happen. The read amplification will eventually kill your performance.

2. Embrace the Kernel Bypass

For high-performance systems, the Linux kernel is often the bottleneck. Technologies like io_uring and SPDK are no longer “experimental”—they are essential tools for anyone targeting sub-millisecond latencies on modern NVMe drives.

3. Privacy is a Data Layout Problem

We often treat privacy as a policy or an encryption layer. In reality, modern privacy (like DP) is much easier to implement when your data is structured for high-performance aggregation. Columnar stores are inherently more “privacy-ready” than row stores.

4. Tiering is the Only Way to Scale

You cannot keep everything in RAM. You cannot put everything on Disk. A sophisticated, multi-tier strategy that understands the temporal value of data is the only way to balance cost and performance at the petabyte scale.


What’s Next?

The move beyond k-anonymity is just the beginning. With our new infrastructure, we are experimenting with on-device feature transformation and even more aggressive federated learning techniques.

By optimizing the lowest levels of our storage stack, we’ve freed our ML engineers to build more complex, more relevant, and more private models. We’ve proven that you don’t have to trade user privacy for system performance—you just need a better way to store the bytes.

Pinterest is always looking for engineers who love diving deep into the stack. If building the future of real-time infrastructure excites you, check out our careers page!


More to explore

Keep diving in