After studying this material, you should be able to:
Imagine teaching someone using one language, then testing them in a slightly different dialect. They will underperform — not because they lack knowledge, but because the format changed.
This is exactly what happens in ML systems.
Training Data: Serving (Inference) Data:
language = "en-US" ≠ language = "en"
region = "jp_JA" ≠ region = "jp-JA"
The model learned patterns from one format. At prediction time, it receives a different format. The model silently degrades.
There is no crash, no error message. The system keeps running. But:
ONLINE PATH (Inference/Serving)
─────────────────────────────────────────────────────
User Request → Feature Store → Model → Prediction
↑
Real-time features
(user_id, language, store metadata)
OFFLINE PATH (Training)
─────────────────────────────────────────────────────
App Logs + Hive Tables + Click Logs
↓
ETL Transformations
↓
Training Dataset → Model Training
The problem: These two paths compute features independently, introducing subtle mismatches.
| Problem | Description |
|---|---|
| Fragile ETL lineages | Missing partitions or upstream changes silently corrupt training data |
| Freshness gaps | Features can be days old before reaching training pipelines |
| Developer productivity loss | Engineers spend weeks debugging instead of building |
Log the exact features used at inference time, then use those same logged features to train the next model.
BEFORE (inconsistent):
Online Pipeline → Features A → Prediction
Offline Pipeline → Features B → Training
↑
A ≠ B = Problem
AFTER (consistent):
Online Pipeline → Features A → Prediction
↓
Log Features A
↓
Offline Pipeline → Features A → Training
↑
A = A = ✓
This creates a single source of truth: the features used for prediction become the features used for training.
Feature logging sounds simple. At Uber's scale, it becomes an enormous engineering challenge.
Traffic: 8,000,000 predictions per second (8M QPS)
Daily rows: ~100 billion records
Monthly: ~5 trillion rows
Storage: ~1.7 PB per day
Logging everything naively would cost millions of dollars in infrastructure.
Feature names are long and descriptive:
store_unique_identifier_operational_meal_period_context_key
Sending this string billions of times per day across the network is extremely wasteful.
Estimated outbound data rate: ~9.3 GiB/second
Kafka cluster capacity: far exceeded
Clients send more features than the model actually uses. Logging all of them wastes bandwidth and storage.
In a recommendation system:
Candidates scored: 1,000
After filtering/ranking: 400 (40%)
Actual user impressions: 20 (5% of ranked)
Logging all 1,000 candidates when only 20 matter wastes 98% of storage on data that adds little training value.
Only log features the model actually uses.
Before: Log all 200 features in request payload
After: Log only 40 features the model needs
Result: 4–5× reduction in payload size
Replace long string names with compact integer IDs.
Before: "store_unique_identifier_operational_meal_period_context_key"
After: 42
Transmitted billions of times → massive bandwidth savings
The mapping is deterministic, so the integer can always be decoded back to the original name.
Only log predictions that actually became user impressions.
Stream 1: Prediction events (from inference)
Stream 2: Client impression events (from user devices)
↓
Flink Stream Join
(time-windowed, few minutes)
↓
Only matched records are logged
Key engineering details:
| Decision | Reason |
|---|---|
| Time window = 90th percentile of session-to-impression time | Minimizes memory while capturing most events |
| Parallelism: 512 for pre-join, 768 for join operator | Each operator has unique scaling needs |
| RocksDB replaced with custom state management | Default checkpointing grew to 12 TB/hour, causing failures |
| Aggressive record eviction after join | Reduces state footprint dramatically |
Transformer models output arrays of candidates, not single rows:
store_uuid: [taco_store, McD, burger_store, ...]
Sending arrays directly breaks Kafka payload limits and complicates impression filtering.
Solution: Flatten arrays into individual key-value records before sending to Kafka.
Before: {store_uuid: [taco_store, McD, burger_store]}
After: {store_uuid: taco_store}
{store_uuid: McD}
{store_uuid: burger_store}
Additionally, data is sharded across multiple Kafka clusters in round-robin fashion to distribute load.
These lessons apply broadly beyond this specific system.
Increasing parallelism alone does not solve bottlenecks.
Each operator in a pipeline has unique scaling characteristics. Profile first, then tune each stage independently based on data.
Before tuning, invest in detailed metrics:
This replaces trial-and-error with data-driven engineering.
No single optimization solved the performance problem. The gains came from combining:
Object reuse
+ Typed payloads
+ Configuration caching
+ Reduced serialization overhead
─────────────────────────────
= ~70% reduction in peak consumer lag
Reliable systems require:
| Metric | Before | After |
|---|---|---|
| Feature mismatch rate | >10% on key features | 0% |
| Feature freshness SLA | Multi-day latency | Hours |
| Payload size | Baseline | 4–5× smaller |
| Peak consumer lag | Baseline | ~70% reduction |
PROBLEM
Training and serving pipelines compute features independently
→ Silent mismatches → Model degradation
SOLUTION
Log exact inference features → Use them for training
→ Single source of truth
SCALE CHALLENGES & SOLUTIONS
┌─────────────────────────┬──────────────────────────────┐
│ Challenge │ Solution │
├─────────────────────────┼──────────────────────────────┤
│ Massive data volume │ Impression filtering (Flink) │
│ Verbose feature names │ Enum aliasing │
│ Unnecessary features │ Feature allow list │
│ Transformer arrays │ Flatten + Kafka sharding │
│ State management │ Custom RocksDB strategy │
└─────────────────────────┴──────────────────────────────┘
OUTCOME
0% mismatch, hours-fresh features, sustainable infrastructure cost