Feature Stores Explained: Do You Actually Need One?
Feature stores promise to solve training-serving skew and enable feature reuse. But they add real complexity. Understand what they actually do, when they pay off, and when they do not.
Feature stores have become a buzzword in MLOps. Every ML platform team eventually builds one or buys one. But most small teams add them prematurely and spend months integrating a system that solves a problem they do not yet have. Here is an honest breakdown.
The Problem Feature Stores Solve
Imagine you are building a churn prediction model. Your training pipeline pulls data from a data warehouse and computes features: rolling 30-day engagement, days since last login, payment failure count. Your model trains on these.
At serving time, your application needs to make a prediction in real-time. It has to recompute the same features - from the same raw signals - and feed them to the model.
Two things go wrong:
1. Training-serving skew. The SQL you wrote for the warehouse computed "rolling 30 days" inclusive of the query date. The Python code for serving computed it exclusive. Your model was trained on subtly different values than it receives at prediction time. Accuracy degrades silently.
2. Feature duplication. Another team builds a fraud model. They need the same rolling engagement features. They recompute them, introducing a second implementation that drifts from yours over time.
A feature store solves both:
- One place to define features
- Same code path for training retrieval and serving retrieval
- Point-in-time correctness for historical lookups (preventing data leakage)
The Architecture
A feature store has two stores:
┌────────────────────────────────────────────────────────────┐
│ Feature Store │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ Offline Store │ │ Online Store │ │
│ │ (historical data) │ │ (low-latency lookup) │ │
│ │ │ │ │ │
│ │ BigQuery / S3 / │ │ Redis / DynamoDB / │ │
│ │ Parquet files │ │ Cassandra │ │
│ └──────────────────────┘ └──────────────────────────┘ │
│ │
│ Feature Registry: definitions, lineage, metadata │
└────────────────────────────────────────────────────────────┘
Training pipeline ──→ reads from Offline Store (historical, point-in-time)
Online serving ──→ reads from Online Store (latest values, <10ms latency)
Feature pipeline ──→ writes to both stores on schedule
A Concrete Example with Feast
Feast is the most widely used open-source feature store.
Define features:
python# features.py from feast import Entity, FeatureView, Field, FileSource from feast.types import Float64, Int64 from datetime import timedelta # Entity: the thing being described customer = Entity(name="customer_id", description="Customer identifier") # Source: where raw data lives customer_stats_source = FileSource( path="s3://ml-data/customer_stats.parquet", timestamp_field="event_timestamp" ) # Feature view: computed features over an entity customer_engagement_fv = FeatureView( name="customer_engagement", entities=[customer], ttl=timedelta(days=30), schema=[ Field(name="rolling_30d_sessions", dtype=Int64), Field(name="days_since_last_login", dtype=Float64), Field(name="payment_failure_count", dtype=Int64), ], source=customer_stats_source )
Training: fetch historical features with point-in-time joins:
pythonfrom feast import FeatureStore import pandas as pd store = FeatureStore(repo_path=".") # Your entity dataframe: who to look up, and when entity_df = pd.DataFrame({ "customer_id": [1001, 1002, 1003], "event_timestamp": [ "2024-11-01", "2024-11-15", "2024-12-01" ] }) # Feast retrieves the feature values as of each event_timestamp # This prevents leakage - no future data bleeds in training_data = store.get_historical_features( entity_rows=entity_df, features=[ "customer_engagement:rolling_30d_sessions", "customer_engagement:days_since_last_login", "customer_engagement:payment_failure_count" ] ).to_df()
Serving: fetch latest features in real-time:
pythonfrom feast import FeatureStore store = FeatureStore(repo_path=".") # Millisecond-latency lookup from Redis feature_vector = store.get_online_features( features=[ "customer_engagement:rolling_30d_sessions", "customer_engagement:days_since_last_login", "customer_engagement:payment_failure_count" ], entity_rows=[{"customer_id": 1001}] ).to_dict() features_array = [ feature_vector["rolling_30d_sessions"][0], feature_vector["days_since_last_login"][0], feature_vector["payment_failure_count"][0] ] prediction = model.predict([features_array])
Same feature definitions, same values, no skew.
Point-in-Time Correctness: Why It Matters
Without a feature store's point-in-time join, your training pipeline might look like:
sql-- WRONG: joins on customer_id only, not on time -- "days_since_last_login" is computed as of today, not as of the event SELECT c.customer_id, f.days_since_last_login, c.churned FROM churn_events c JOIN customer_features f ON c.customer_id = f.customer_id
This leaks future information into the training set. The model sees features computed after the event happened. It will appear accurate in training but fail in production because it was trained on data it cannot have.
Point-in-time joins look up the feature value that existed at the time of the event:
sql-- RIGHT: for each event at time T, get the feature value that existed just before T SELECT c.customer_id, f.days_since_last_login, c.churned FROM churn_events c JOIN customer_features f ON c.customer_id = f.customer_id AND f.timestamp <= c.event_timestamp -- feature existed before the event ORDER BY f.timestamp DESC -- (in practice: take the latest f.timestamp <= c.event_timestamp per customer)
Feast handles this automatically.
When You Actually Need a Feature Store
Use one when:
- Multiple models share features and recomputing them independently is causing drift or wasted work
- You have training-serving skew and have proven it is hurting model performance
- You have real-time features that need sub-10ms lookup and you have multiple serving services consuming them
- Your team is >5 ML engineers and coordination cost is real
Do not use one when:
- You have one or two models in production
- All your features are batch (computed nightly, served from a database column)
- You can achieve the same thing with a well-named database table and a shared feature computation library
The Simpler Alternative for Small Teams
For many teams, this is sufficient:
python# features/compute.py - one module, imported by both training and serving def compute_customer_features(customer_id: int, as_of_date: str, conn) -> dict: """Single source of truth for customer features.""" query = """ SELECT COUNT(*) FILTER ( WHERE session_date > %(as_of)s::date - 30 AND session_date <= %(as_of)s::date ) AS rolling_30d_sessions, (%(as_of)s::date - MAX(session_date))::int AS days_since_last_login, COUNT(*) FILTER ( WHERE event_type = 'payment_failure' AND event_date <= %(as_of)s::date ) AS payment_failure_count FROM customer_events WHERE customer_id = %(customer_id)s """ return conn.execute(query, {"as_of": as_of_date, "customer_id": customer_id}).fetchone()
Same function, called in training scripts with historical dates and in serving code with today's date. No additional infrastructure, no ops burden, genuinely shared logic.
Start here. Graduate to a feature store when this breaks down.
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsOpen-Weight and Small Models in 2026: When to Self-Host
Open-weight models are competitive, small models run on a phone, and the API-for-everything default is no longer obviously right. Here is a decision framework for self-hosting versus API, where small models win, what mixture-of-experts changes about the parameter count, and the hybrid most teams end up with.
ML Model to Production: A Complete Walkthrough
Most ML models die in notebooks. Walk through the full path from trained model to live API endpoint serving real traffic - packaging, containerizing, deploying, and monitoring.
Model Versioning with MLflow: Practical Guide
Without model versioning, you cannot reproduce results, roll back broken deployments, or compare experiments. MLflow gives you a practical registry - here is how to use it well.