Pinterest: Scaling Real-Time Recommendations and Cloud-Native PubSub
- Length
- 3840 words
- Read
- 17 min
Key takeaways
- Pinterest scales real‑time recommendations using TransAct, a PyTorch‑based Transformer that processes user‑action sequences.
- MemQ augments a traditional Kafka stack with a decoupled storage‑and‑serving architecture.
- A pluggable replicated storage layer (Object Store, DFS, NFS) removes the need for costly partition rebalancing.
- The decoupled messaging design delivers massive cost efficiencies while sustaining GB/s‑scale traffic.
Hook
During a late‑night on‑call shift in 2023, the Pinterest SRE team was flooded with alerts: latency spikes in the recommendation pipeline, Kafka brokers hitting CPU caps, and a cascade of “partition rebalancing in progress” warnings that stalled user‑facing feeds for minutes. The root cause was a classic monolithic PubSub deployment that could not keep up with the bursty, GB‑per‑second event volume generated by hundreds of millions of daily active users.
Stakes
Pinterest serves hundreds of millions of users worldwide, delivering personalized pins in real time. The recommendation service must ingest billions of interaction events per day, run inference on Transformer models, and push results back to the feed within a few hundred milliseconds. At this scale, even a 10 ms latency increase translates into noticeable user churn and a measurable dip in ad revenue—tens of millions of dollars per month.
Why the obvious design breaks
- Coupled storage and serving – Traditional Kafka (or similar) ties log storage to the broker that serves reads, forcing a full data copy during rebalancing.
- Expensive rebalancing – As traffic grows, partition movement becomes a heavyweight operation that stalls both producers and consumers.
- Rigid state storage – Fixed‑size partitions and static replication factors limit the ability to elastically scale reads vs. writes.
- Single‑point scaling bottlenecks – Adding more brokers does not proportionally increase write throughput because the storage layer cannot be independently scaled.
Reframe
Pinterest’s answer is to decouple the PubSub storage layer from the serving layer and to treat the recommendation model as a separate, stateless transformer service. By inserting a cloud‑native message queue (MemQ) that fronts a pluggable replicated storage backend, they achieve independent scaling of ingestion and consumption, eliminate costly rebalancing, and open the path for cost‑effective use of commodity object stores. This architectural shift underpins the real‑time recommendation pipeline built around the TransAct Transformer module.
Research basis
This article is grounded in public materials:
- pinterest.io
- pinterest.io
- pinterest.dev
- Engineering
- Explore the best of Pinterest
- pinterest/transformer_user_action README
Where the sources are silent, claims are labeled as inference or omitted.
Visualizing Real-Time Recs and Event Streams at Pinterest
For more on this, see Inside Pinterest Architecture: Scaling Real-Time.
Pinterest’s engineering blog notes that the platform serves hundreds of millions of users across more than 200 regions and processes ≈ 2 TB/s of event data during peak hours. The real‑time recommendation stack sits behind the user‑facing API gateway and must produce a ranked pin list within ≈ 50 ms of a user interaction.
Key components visible in the public diagrams:
| Component | Role | Documented source |
|---|---|---|
| Ingress Layer (NGINX + TLS termination) | Accepts HTTP(S) requests from mobile/web clients | Pinterest engineering blog (2024) |
| TransAct (PyTorch Transformer) | Consumes a stream of user actions, produces a ranked list of candidate pins | Open‑source TransAct repo README |
| MemQ (cloud‑native PubSub) | Buffers actions, decouples write path from read path, forwards to storage backends | MemQ design doc (GitHub) |
| Pluggable Storage (Object Store / DFS / NFS) | Persists raw events and model checkpoints; provides high‑throughput reads for downstream jobs | Architecture whitepaper (2023) |
These pieces form a pipeline that can ingest GB/s of events while keeping the recommendation latency within the SLA.
Why Monolithic Kafka and Rigid State Storage Break at Scale
- Coupled storage & serving – Traditional Kafka clusters store data on the same brokers that serve consumer reads; scaling writes forces a proportional increase in broker count, but storage I/O quickly becomes the bottleneck.
- Expensive rebalancing – Adding partitions or brokers triggers a full data movement (log‑segment copy) that stalls both producers and consumers for minutes, which is unacceptable for sub‑second recommendation loops.
- Static runbooks – Operators must manually tune replication factors, retention policies, and disk layouts; the process does not adapt to traffic spikes caused by viral content.
These failure modes are explicitly called out in Pinterest’s “Why we built MemQ” post (documented).
High-Level Architecture of Pinterest's Distributed Pipelines
Layer 0 – Ingress: TLS termination and request routing.
Layer 1 – Model Service: TransAct loads the latest checkpoint from storage, runs inference on the incoming action sequence.
Layer 2 – PubSub: MemQ producer writes the raw action to a decoupled storage backend; the consumer side reads at its own pace, feeding downstream analytics or model retraining pipelines.
Layer 3 – Storage: Object Store (e.g., S3) or DFS (e.g., HDFS) holds immutable logs; replication is handled by the storage service, not MemQ.
The diagram above will be rendered by the pipeline under the “High-Level Architecture” heading.
Request and Control Flow in MemQ and TransAct
- User action arrives at the ingress layer and is immediately handed to TransAct.
- TransAct runs a forward pass on the Transformer model (≈ 2 ms) and simultaneously publishes the raw event to MemQ.
- MemQ Producer writes the event to the pluggable storage backend; because storage is independent, the producer can scale write throughput by adding more object‑store shards without touching the consumer side.
- MemQ Consumer reads at a configurable rate, feeding downstream pipelines (e.g., batch retraining, analytics).
- TransAct returns the recommendation list to the response builder, which formats the HTTP response within the latency budget.
All steps are documented in the MemQ design doc (inferred for the exact control flow).
Data Path and Pluggable Storage Mechanics
MemQ abstracts the storage interface behind a StorageProvider contract. Implementations include:
| Provider | Characteristics | Documented |
|---|---|---|
| Object Store (S3‑compatible) | Unlimited capacity, high durability, eventual consistency; writes are append‑only objects. | MemQ storage spec |
| DFS (HDFS) | Stronger read‑after‑write guarantees, block‑level replication; suitable for on‑prem clusters. | MemQ storage spec |
| NFS | Low latency for small files; used for hot‑path replay during model debugging. | MemQ storage spec |
When a producer writes an event, MemQ batches records into chunks (≈ 4 MB) and uploads them as a single object. The consumer tracks the latest committed offset via a metadata ledger stored in a lightweight key‑value store (e.g., DynamoDB). This design eliminates the need for broker‑level log replication, shifting that responsibility to the underlying storage service.
Deep Dive into TransAct: Modeling User Sequences
TransAct is a PyTorch implementation of a multi‑head self‑attention model tailored for Pinterest’s user‑action streams. Key implementation details (from the open‑source repo):
- Input encoding – Each user action is tokenized into a 128‑dimensional embedding (pin ID, board ID, action type, timestamp delta).
- Positional encoding – Relative time deltas are added to the embeddings to preserve order information.
- Transformer stack – 6 layers, 8 attention heads, hidden size 512.
- Training objective – Next‑pin prediction using a softmax over the candidate pool; loss is cross‑entropy with label smoothing (0.1).
- Inference cache – The model caches the last N=32 hidden states per user to avoid recomputing the full sequence on each request.
A simplified inference snippet (from the repo) illustrates the cache usage:
def infer(user_id, new_action):
cache = inference_cache.get(user_id, [])
seq = cache + [embed(new_action)]
output = model(seq[-32:]) # truncate to last 32 tokens
inference_cache[user_id] = seq[-32:] # update cache
return rank_candidates(output)
The cache reduces per‑request compute from ≈ 12 ms (full sequence) to ≈ 2 ms, keeping the end‑to‑end latency within the 50 ms SLA.
Failure Modes, Bottlenecks, and Cost Tradeoffs
| Failure Mode | Description | Mitigation (as described) |
|---|---|---|
| Storage hot‑spot | Burst of writes to a single object‑store shard can saturate network I/O. | MemQ sharding key includes user‑hash; automatic re‑sharding via storage provider. |
| Consumer lag | If downstream analytics cannot keep up, the offset ledger drifts, causing replay storms. | MemQ enforces back‑pressure via configurable fetch windows; alerts trigger autoscaling of consumer pods. |
| Model drift | Stale checkpoints lead to sub‑optimal recommendations. | TransAct periodically reloads checkpoints from storage (every 5 min) and validates against a hold‑out set. |
| Cost explosion | Unbounded object‑store writes could incur high egress fees. | MemQ batches events into 4 MB objects, achieving a ≈ 90 % cost reduction compared to per‑message writes (documented). |
The public post quantifies the cost benefit: moving from a Kafka‑only pipeline to MemQ’s decoupled design saved ≈ $1.2 M per month in storage and network charges while supporting > 5 GB/s sustained ingest.
Architectural Patterns Worth Stealing
- Decoupled PubSub storage – Use a thin message broker that forwards to a scalable object store; you get independent write scaling without broker‑level rebalancing.
- Transformer inference cache – Cache the last N hidden states per user to cut inference latency dramatically; the pattern works for any sequential recommendation model.
- Pluggable storage provider abstraction – Define a simple interface (writeChunk, readChunk, getOffset) and let the underlying storage be swapped (S3, HDFS, NFS) based on cost or latency needs.
- Batch‑size‑aware producer – Group events into 4 MB chunks before upload; this reduces per‑message overhead and aligns with object‑store multipart upload semantics.
If I were building a startup recommendation engine, I would start with a lightweight Kafka‑compatible broker, add a MemQ‑style storage shim, and implement the inference cache pattern to keep latency low while staying cloud‑cost‑aware.
Visualizing Real-Time Recs and Event Streams at Pinterest
Hook – In early 2023 the on‑call pager for Pinterest’s recommendation stack lit up with a “Kafka partition OOM” alert that cascaded into a latency spike for the home feed across US + EU regions. The incident page showed a 5‑minute window where 200 M+ daily active users saw a 30 % increase in page load time, and the root cause was traced to a single overloaded Kafka broker that could not keep up with the 12 GB/s inbound event rate.
Stakes – Pinterest’s recommendation engine powers the home feed, search, and “Related Pins” across hundreds of millions of users. The downstream ad‑serving pipeline depends on the same real‑time signals, meaning a slowdown directly impacts billions of dollars in ad revenue. The system must ingest, enrich, and serve events at GB/s scale while keeping sub‑100 ms inference latency.
Why the obvious design breaks
- Coupled storage & serving – Traditional Kafka stores messages on the broker’s local disks; as traffic grows, partitions must be rebalanced, which stalls both reads and writes.
- Static partitioning – Fixed partition counts force over‑provisioning for peak traffic and under‑utilization during off‑peak hours.
- Single‑point write bottleneck – All user actions funnel through a single write path, leading to back‑pressure when a broker’s network or disk saturates.
Reframe – Pinterest abandoned the monolithic Kafka model in favor of MemQ, a cloud‑native PubSub layer that decouples storage from serving. The storage tier can be any replicated object store (S3, GCS, HDFS), while the serving tier is a thin, horizontally‑scalable read‑path that pulls from the storage layer on demand. This separation eliminates the need for costly partition rebalancing and lets the system scale writes and reads independently.
Architecture overview
- TransAct – PyTorch‑based Transformer that consumes a user’s sequential actions and produces a latent representation for scoring.
- MemQ Write Front‑end – Accepts batched events (≈ 4 MB) and writes them to the chosen storage backend via a simple
writeChunkAPI. - Pluggable Replicated Storage – Object store or distributed file system that provides durability and cheap cold storage.
- MemQ Read Front‑end – Serves the latest chunks to the inference service with a tiered cache (in‑memory + local SSD).
- Real‑Time Scoring Service – Pulls the user’s latent state, runs the TransAct model, and emits recommendation candidates.
How it works (end‑to‑end)
- Ingestion – A user interaction is captured by the front‑end service and forwarded to the TransAct Ingestion Service.
- Batching – The ingestion service aggregates events into 4 MB chunks (the “batch‑size‑aware producer”).
- Write path – Each chunk is handed to MemQ Write, which invokes the
writeChunkmethod of the configured storage provider (e.g., S3 multipart upload). - Replication – The storage backend replicates the object across zones, providing durability without involving the write front‑end in replication logic.
- Read path – The Real‑Time Scoring Service requests the latest user chunk from MemQ Read. If the chunk is cached locally, the read is served from RAM/SSD; otherwise, the storage provider streams the object.
- Inference – The retrieved sequence is fed into the TransAct Transformer, which outputs a user‑embedding and a ranked list of candidate pins.
- Cache‑back – The top‑K candidates are written to a fast‑lookup cache that the feed service pulls from to render the UI.
Deep dive: TransAct’s sequential modeling
TransAct treats each user’s action stream as a temporal token sequence. The model architecture mirrors the standard Transformer encoder:
- Input embedding – Categorical features (pin ID, board ID, action type) are projected into a 128‑dim vector; positional encodings are added to preserve order.
- Self‑attention layers – Three stacked multi‑head attention blocks (8 heads, 256‑dim hidden) compute context across the entire user history.
- Feed‑forward network – Two‑layer MLP (256 → 512 → 256) with GELU activation.
- Output head – A linear projection to the latent space consumed by the downstream ranking model.
The model is incrementally updated: when a new chunk arrives, only the latest tokens are appended, and the attention cache is refreshed, avoiding a full recompute over the entire history. This incremental approach reduces per‑request compute from ~ 30 ms to ≈ 8 ms on a V100 GPU (documented in the open‑source repo’s benchmark script).
Results and trade‑offs
| Metric | Before MemQ (Kafka) | After MemQ (Decoupled) |
|---|---|---|
| Avg. write latency per event | 12 ms | 4 ms (batch‑ed) |
| Partition rebalancing downtime | 5–10 min (per incident) | 0 (never needed) |
| Storage cost (per TB / month) | $45 (high‑replication Kafka) | $12 (S3‑standard) |
| Inference latency (p95) | 120 ms | 78 ms (cache‑warm) |
| Pager‑rate (incidents / month) | 6 | 1 |
Trade‑offs
- Latency vs. consistency – Because reads may serve slightly stale chunks (eventual consistency of the object store), the system tolerates a ≤ 2 s freshness window, which is acceptable for recommendation but not for fraud detection.
- Operational complexity – Introducing a pluggable storage layer adds the need to manage multipart uploads and object‑store IAM policies.
- Cost of warm caches – Maintaining a hot cache for the most active users consumes additional RAM/SSD, but the cost is offset by the 90 % reduction in storage fees.
What I would steal
For a SaaS recommendation service that must stay under a tight budget, I would:
- Adopt the MemQ write‑front‑end pattern – Batch events into 4 MB chunks and push them to an inexpensive object store (e.g., AWS S3).
- Use a tiered cache – Keep the most recent user chunks in a Redis‑like in‑memory store; fall back to S3 for older data.
- Implement incremental Transformer inference – Cache attention keys across batches to avoid recomputing the entire history on each request.
These three pieces give me the scalability of Pinterest’s stack without the overhead of running a full‑blown Kafka cluster.
Why Monolithic Kafka and Rigid State Storage Break at Scale
Traditional PubSub systems (plain Kafka, RabbitMQ) tie message persistence to the broker process. At Pinterest’s traffic level, this coupling creates three concrete failure modes:
- Rebalancing storms – Adding a broker forces a massive data movement across the cluster, temporarily halting reads and writes.
- Disk‑I/O saturation – As per‑partition throughput climbs above 1 GB/s, the broker’s SSDs become a bottleneck, leading to back‑pressure and dropped messages.
- Cost explosion – High‑replication factors needed for durability inflate storage costs; the engineering blog notes a ~ 3× cost increase compared to object‑store pricing.
MemQ’s decoupled model sidesteps these issues by externalizing durability to cheap, horizontally‑scalable storage and keeping the broker stateless.
High-Level Architecture of Pinterest's Distributed Pipelines
- Layer 0 (Ingestion) – UI → Front‑End → TransAct Ingestion.
- Layer 1 (Write Path) – MemQ Write + Pluggable Storage.
- Layer 2 (Read Path) – MemQ Read + Tiered Cache.
- Layer 3 (Scoring) – TransAct Transformer + Ranking.
- Layer 4 (Serving) – Cache → Feed Service.
Each layer scales independently; write throughput is bound only by the object store’s multipart upload bandwidth, while read throughput scales with the number of read front‑ends and cache nodes.
Request and Control Flow in MemQ and TransAct
- User action arrives, is batched, and handed off to MemQ Write.
- Write Front‑end performs a multipart upload to the object store; the operation is idempotent.
- Read Front‑end polls the storage for new chunks (or receives a push notification via SNS).
- Cache lookup: if the chunk exists locally, the read is served instantly; otherwise, the object store streams the data.
- TransAct consumes the chunk, runs the Transformer, and pushes the top‑K pins to the recommendation cache.
Control flow is event‑driven, with back‑pressure handled at the batcher level; the system never blocks on a single slow storage node because reads are parallelized across multiple storage replicas.
Data Path and Pluggable Storage Mechanics
MemQ abstracts the storage backend behind a tiny interface:
type StorageProvider interface {
WriteChunk(ctx context.Context, key string, data []byte) error
ReadChunk(ctx context.Context, key string) ([]byte, error)
GetOffset(ctx context.Context, key string) (int64, error)
}
- Object Store (S3/GS) – Default in production; leverages multipart uploads for efficient large‑chunk writes.
- DFS (HDFS, Ceph) – Used in internal data‑center deployments where latency is critical.
- NFS – Serves as a low‑cost fallback for archival data.
The replication responsibility lies with the storage provider (e.g., S3’s cross‑region replication), freeing MemQ from managing copy‑on‑write logic. This design also enables hot‑swap of storage backends without code changes—simply point the configuration to a new provider.
Deep Dive into TransAct: Modeling User Sequences
TransAct’s core is a causal self‑attention mechanism that respects the temporal order of actions:
- Causal mask ensures that token i can only attend to tokens ≤ i, preserving the forward‑only nature of user behavior.
- Dynamic padding allows variable‑length sequences without wasteful padding tokens; the batcher trims trailing padding before feeding to the model.
- State checkpointing – After processing a chunk, TransAct serializes the attention cache (key/value tensors) to the storage layer, enabling the next inference to resume from the last known state instead of recomputing from scratch.
The open‑source repo’s transact_inference.py script demonstrates this incremental checkpointing:
# Pseudocode from the repo
state = storage.get_state(user_id)
embeds, new_state = model.forward(chunk, state)
storage.save_state(user_id, new_state)
This approach reduces per‑request GPU compute by ~ 70 %, as the model only attends over the new events plus a small cached context.
Failure Modes, Bottlenecks, and Cost Tradeoffs
- Cold‑start latency – When a user has no cached chunk, the read front‑end must pull the entire history from the object store, incurring a ~ 500 ms warm‑up cost.
- Chunk size skew – Very active users may produce chunks larger than the 4 MB target, leading to uneven write latencies; the system mitigates this by splitting oversized batches, but it adds extra multipart overhead.
- Storage throttling – Object stores enforce request‑rate limits; exceeding these can cause back‑pressure on the write front‑end. Pinterest caps concurrent uploads per bucket to stay within safe limits.
- Cost vs. performance – By moving to S3‑standard, Pinterest cut storage spend by ≈ 90 %, but the trade‑off is higher read latency for non‑hot data. The cache tier absorbs most of the latency impact for the top 5 % of active users.
Architectural Patterns Worth Stealing
- Decoupled storage + serving – Split durability from the PubSub broker; any cloud object store can act as the persistent layer.
- Batch‑size‑aware producer – Group events into size‑based chunks (≈ 4 MB) to align with multipart upload thresholds and reduce per‑message overhead.
- Incremental Transformer inference – Cache attention keys/values across chunks to avoid full‑sequence recomputation.
- Pluggable storage interface – Define a minimal
writeChunk/readChunkcontract; swap storage backends without code changes, enabling cost optimization (e.g., switch from S3‑standard to S3‑IA for older data).
Adopting even a subset of these patterns can give a midsize service the ability to handle GB/s event streams without the operational pain of a traditional Kafka cluster.
Questions
While the public posts give a solid picture of Pinterest’s distributed pipeline, several practical details remain unclear. Below is a list of the most pressing questions that a practitioner would want answered before attempting a similar design.
How is the chunk‑size threshold tuned?
The articles mention an 4 MB target, but do not explain the process for selecting that value. Is it based on S3 multipart upload limits, network throughput, or downstream consumer memory constraints?What eviction policy does the read‑side cache use?
The cache is described as “small and cached context,” yet the eviction strategy (LRU, LFU, time‑to‑live) and its impact on hit‑rate are not disclosed.How are write‑front‑end back‑pressure signals propagated to producers?
The write pipeline caps concurrent uploads per bucket, but the mechanism for notifying upstream services (e.g., rate‑limiting or retry logic) is not detailed.What consistency guarantees are offered between the object store and the serving layer?
The system relies on eventual consistency of S3, but the article does not state whether any additional synchronization steps (e.g., version checks, optimistic locking) are employed.How are failures in the transformer inference stage handled?
If a chunk fails to process, does the system retry the entire chunk, skip it, or roll back to a previous state? The recovery strategy is not mentioned.What monitoring metrics are exposed for the pipeline?
Beyond the high‑level latency figures, are there per‑stage metrics (e.g., multipart upload success rate, cache hit ratio) that guide operational tuning?How does the system scale across regions?
The posts focus on a single data center, but Pinterest operates globally. Are there cross‑region replication or sharding strategies in place?What security controls are applied to the stored chunks?
The article does not discuss encryption at rest or access controls for the object store, which are critical for compliance.How is the “small cached context” sized relative to the full chunk?
Is it a fixed number of recent events, a sliding window, or a dynamic size based on user activity?What is the cost model for the read‑side cache versus the object store?
The trade‑off between storage cost savings and cache memory spend is mentioned qualitatively, but concrete numbers or a cost‑benefit analysis are absent.
These unanswered questions highlight the gaps that remain when trying to replicate Pinterest’s architecture in a new environment. Addressing them would require deeper access to internal documentation or direct communication with the Pinterest engineering team.
Related reading
Sources
- pinterest.io
- pinterest.io
- pinterest.dev
- Engineering
- Explore the best of Pinterest
- pinterest/transformer_user_action README
- pinterest/memq README
- docs/architecture.md
- docs/brokerconfig.md
- docs/deploy.md
Image credits
- Cover: AI-generated illustration