Building Real-Time Feature Stores for Machine Learning
Your fraud detection model is brilliant. It catches 94% of fraudulent transactions in offline evaluation. Then you deploy it and the performance tanks — not because the model is wrong, but because…
Building Real-Time Feature Stores for Machine Learning
Your fraud detection model is brilliant. It catches 94% of fraudulent transactions in offline evaluation. Then you deploy it and the performance tanks — not because the model is wrong, but because the features it's getting in production are stale, inconsistent, or just plain different from what it trained on. That's the training-serving skew problem, and it's one of the most painful issues in production ML.
Feature stores exist to solve this. And if you're building real-time ML applications — fraud detection, recommendation engines, dynamic pricing, personalization — you need to understand how to build one that actually performs under pressure.
What a Feature Store Actually Does
A feature store is infrastructure that sits between your raw data and your ML models. It has two jobs:
The critical insight is that both jobs need to serve the *same* features. That consistency is the whole point.
A typical feature store has three layers:
The dual-store pattern is worth understanding deeply. You use something like Apache Cassandra or Redis for online lookups (sub-millisecond reads), and Apache Parquet on S3 or BigQuery for offline training data. The trick is keeping them in sync.
The Architecture in Practice
Here's a simplified architecture for a real-time feature store:
Raw Events → Kafka → Feature Pipeline (Flink/Spark Streaming)
↓
┌─────────────────┐
│ Feature Store │
├────────┬────────┤
│ Online │Offline │
│ Redis │ S3 │
└────────┴────────┘
↓
Feature Serving API
↓
Model Inference ServiceEvents flow in through Kafka. A streaming pipeline (Flink is popular here) computes features and writes to both stores simultaneously. This dual-write is how you avoid skew — the same computation produces both the training data and the serving data.
Building the Feature Pipeline
Let's look at a concrete example. Say you're computing user-level features for a recommendation system — things like "number of purchases in the last 24 hours" or "average session duration this week."
Here's a simplified Flink job that computes a rolling feature:
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common.time import Timeenv = StreamExecutionEnvironment.get_execution_environment()
Read from Kafka
events = env.add_source(kafka_consumer)Compute purchase count per user in 1-hour windows
purchase_features = (
events
.filter(lambda e: e["event_type"] == "purchase")
.key_by(lambda e: e["user_id"])
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.aggregate(PurchaseCountAggregator())
)Write to both online and offline stores
purchase_features.add_sink(RedisSink(host="redis-cluster", port=6379))
purchase_features.add_sink(S3Sink(bucket="feature-store", prefix="purchases/"))env.execute("purchase_feature_pipeline")
The PurchaseCountAggregator would implement AggregateFunction and maintain a running count. The key thing here is that both sinks receive the exact same computed values — no divergence.
Serving Features at Low Latency
The online serving layer needs to be fast. When a model needs features for inference, you typically have a budget of 50-100ms for the entire request, and feature retrieval should consume at most 10-20ms of that.
Here's a simple feature server using FastAPI and Redis:
import redis
import json
from fastapi import FastAPI, HTTPException
from typing import Listapp = FastAPI()
redis_client = redis.Redis(host="redis-cluster", port=6379, decode_responses=True)
@app.get("/features/{entity_id}")
async def get_features(entity_id: str, feature_names: List[str]):
pipeline = redis_client.pipeline()
for feature_name in feature_names:
key = f"feature:{feature_name}:{entity_id}"
pipeline.get(key)
results = pipeline.execute()
features = {}
for feature_name, value in zip(feature_names, results):
if value is None:
# Return default values for missing features
features[feature_name] = get_default_value(feature_name)
else:
features[feature_name] = json.loads(value)
return {"entity_id": entity_id, "features": features}
Notice the Redis pipeline — this batches all your GET commands into a single round trip. If you're fetching 20 features, you don't want 20 separate network calls. This alone can cut your latency by 10x.
Handling the Hard Parts
Feature freshness is a constant battle. Some features need to be updated every second (current cart value), others every hour (weekly engagement score), others daily (demographic segments). Your pipeline needs to handle different update frequencies gracefully. Tag each feature with a TTL in Redis and a refresh schedule in your orchestration layer.
Backfilling is painful but necessary. When you add a new feature, you need historical values to retrain your model. Design your offline store schema upfront to make point-in-time correct queries possible. This means storing feature values with timestamps and being able to reconstruct "what did this feature look like at time T?"
-- Point-in-time correct feature lookup
SELECT
entity_id,
feature_name,
feature_value
FROM feature_store.feature_history
WHERE entity_id = '12345'
AND feature_name = 'purchase_count_24h'
AND computed_at <= '2024-01-15 14:30:00'
ORDER BY computed_at DESC
LIMIT 1;Default values deserve more thought than they usually get. When a new user hits your system, most of their features won't exist yet. Returning null to your model is almost always wrong — it'll behave unpredictably. Define sensible defaults for every feature (population median, zero, empty list) and handle missing data explicitly in your serving layer.
Don't Reinvent Everything
Before building from scratch, check out the open-source ecosystem. Feast is the most mature open-source feature store and handles a lot of the infrastructure plumbing. Hopsworks offers a managed option. If you're on AWS, SageMaker Feature Store is worth evaluating. These aren't perfect, but they solve the boring problems so you can focus on your actual feature logic.
That said, there are good reasons to build custom — you need tighter latency guarantees, you have unusual data types, or you're operating at a scale where managed solutions get expensive fast.
Practical Tips Before You Ship
Next Steps
If you're starting fresh: set up a local Redis instance and build a simple feature serving API for one use case. Get that working end-to-end before adding complexity. Then introduce Kafka and a streaming pipeline once you understand what your feature freshness requirements actually are.
If you're inheriting an existing system with training-serving skew: audit your feature computation code first. Nine times out of ten, the offline training pipeline and the online serving code are separate implementations that have drifted apart. Consolidating them onto a single computation engine is usually the highest-leverage fix.
Feature stores aren't glamorous, but they're the difference between a model that works in notebooks and one that works in production. Get the infrastructure right and everything else gets easier.