Feature Stores Explained: Why Teams Need Them
A feature store keeps the features a model trains on and the features it is served the same. This guide builds one with Feast on a year of real retail orders and shows the problem it solves: a naive join that looked better in validation, 0.798 against 0.727, then did worse in production.
GloryTecks Team
Training & Placement Faculty
Last updated: 8 October 2026
Two problems appear as soon as a model goes live. Features get computed one way for training and another way in the serving code, so the model sees different numbers in production. And training data quietly includes the future, because someone joined today's values to last year's outcomes. A feature store addresses both.
This guide explains the idea and builds a small store with Feast on a year of real retail orders. It then measures what point-in-time correctness is worth.
What a feature store does
Feast's documentation describes two foundations: an offline store for historical feature extraction used in training, and an online store for serving features at low latency in production. It also promises point-in-time correct feature sets, so that future values do not leak into training (Feast: introduction).
| Part | Holds | Used for |
|---|---|---|
| Registry | Feature definitions: names, entities, sources, types | Sharing and reuse |
| Offline store | Full history with timestamps | Training sets, batch scoring |
| Online store | The latest value per entity | Real-time predictions |
| Materialisation | Jobs copying offline values online | Keeping serving fresh |
Point-in-time correctness
A training row asks what was known about a customer at a moment in the past. Feast answers by joining, for each row, the newest feature values at or before that row's timestamp, scanning backward no further than the feature view's time-to-live (Feast: point-in-time joins). The common mistake is to join a "current" feature table to historical labels: every row then knows things that happened after its own moment.
The experiment
The UCI Online Retail data hold every transaction from 1 December 2010 to 9 December 2011 for a UK-based online gift retailer, many of whose customers are wholesalers, under CC BY 4.0 (UCI: Online Retail). The question: will a customer order again within 30 days? Features are lifetime orders, orders and spend over the last 90 days, and days since the last order, captured as weekly snapshots.
A one-off step keeps valid, identified sales lines:
import pandas as pd
# One-off conversion of the UCI Online Retail workbook (CC BY 4.0) to Parquet, keeping valid, identified sales lines
raw = pd.read_excel("Online Retail.xlsx", dtype={"InvoiceNo": str, "StockCode": str})
NOT_PRODUCTS = {"POST", "DOT", "M", "m", "C2", "D", "S", "B", "BANK CHARGES", "AMAZONFEE", "CRUK"}
sales = raw[raw.CustomerID.notna() & (raw.Quantity > 0) & (raw.UnitPrice > 0)
& ~raw.InvoiceNo.str.startswith("C") & ~raw.StockCode.isin(NOT_PRODUCTS)
& ~raw.StockCode.str.startswith("gift_")].copy()
sales["customer_id"] = sales.CustomerID.astype(int)
sales["revenue"] = sales.Quantity * sales.UnitPrice
sales[["InvoiceNo", "InvoiceDate", "customer_id", "revenue"]].to_parquet("transactions.parquet", index=False)
print(len(raw), "rows read,", len(sales), "kept,", sales.customer_id.nunique(), "customers,",
sales.InvoiceDate.min(), "to", sales.InvoiceDate.max())541909 rows read, 396340 kept, 4334 customers, 2010-12-01 08:26:00 to 2011-12-09 12:50:00The Feast repository uses the local provider, with a SQLite online store:
project: retail
registry: data/registry.db
provider: local
online_store:
type: sqlite
path: data/online_store.db
entity_key_serialization_version: 3Training rows come from the first Monday of each month from April to October 2011; 7 November 2011 plays the part of production.
import contextlib
import io
import os
from datetime import timedelta
import numpy as np
import pandas as pd
from feast import Entity, FeatureStore, FeatureView, Field, FileSource, ValueType
from feast.types import Float32, Int64
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
orders = (pd.read_parquet("transactions.parquet") # UCI Online Retail, cleaned once (see prepare.py)
.groupby(["customer_id", "InvoiceNo"], as_index=False)
.agg(when=("InvoiceDate", "min"), revenue=("revenue", "sum")))
def snapshot(at): # every known customer's features as of `at`, from earlier orders only
past = orders[orders.when < at]
recent = past[past.when >= at - pd.Timedelta(days=90)]
features = past.groupby("customer_id").agg(orders_total=("InvoiceNo", "size"), last=("when", "max"))
features["orders_90d"] = recent.groupby("customer_id").size()
features["spend_90d"] = recent.groupby("customer_id").revenue.sum()
features["days_since_last"] = (at - features.pop("last")).dt.days
features = features.fillna({"orders_90d": 0, "spend_90d": 0.0}).astype({"orders_90d": "int64"})
return features.reset_index().assign(event_timestamp=at.tz_localize("UTC"))
os.makedirs("data", exist_ok=True)
mondays = pd.date_range("2011-01-03", "2011-12-05", freq="W-MON")
weekly = pd.concat([snapshot(day) for day in mondays], ignore_index=True)
weekly.to_parquet("data/customer_weekly.parquet", index=False) # the offline store's source table
customer = Entity(name="customer", join_keys=["customer_id"], value_type=ValueType.INT64)
customer_weekly = FeatureView(
name="customer_weekly", entities=[customer], ttl=timedelta(days=7),
schema=[Field(name="orders_total", dtype=Int64), Field(name="orders_90d", dtype=Int64),
Field(name="spend_90d", dtype=Float32), Field(name="days_since_last", dtype=Int64)],
source=FileSource(path="data/customer_weekly.parquet", timestamp_field="event_timestamp"))
store = FeatureStore(repo_path=".") # feature_store.yaml: local provider, SQLite online store
store.apply([customer, customer_weekly])
NAMES = ["orders_total", "orders_90d", "spend_90d", "days_since_last"]
FEATURES = [f"customer_weekly:{name}" for name in NAMES]
def label_rows(cutoff): # customers known before the cutoff; did they order in the next 30 days?
known = orders[orders.when < cutoff].customer_id.unique()
buyers = orders[(orders.when >= cutoff) & (orders.when < cutoff + pd.Timedelta(days=30))].customer_id.unique()
return pd.DataFrame({"customer_id": known, "event_timestamp": cutoff.tz_localize("UTC"), "bought": np.isin(known, buyers)})
train_rows = pd.concat([label_rows(day) for day in pd.date_range("2011-04-01", "2011-10-31", freq="WOM-1MON")], ignore_index=True)
test_rows = label_rows(pd.Timestamp("2011-11-07"))
print(f"offline store: {len(weekly):,} rows ({len(mondays)} weekly snapshots); "
f"training rows: {len(train_rows):,} from 7 monthly cut-offs; November test rows: {len(test_rows):,}")
point_in_time = store.get_historical_features(entity_df=train_rows, features=FEATURES).to_df()
latest = weekly[weekly.event_timestamp == weekly.event_timestamp.max()] # the "current" table someone might just join
naive = train_rows.merge(latest[["customer_id"] + NAMES], on="customer_id")
print(f"training rows returned: point-in-time {len(point_in_time):,}, naive join {len(naive):,}")
with contextlib.redirect_stdout(io.StringIO()): # materialize prints a progress bar
store.materialize(start_date=pd.Timestamp("2011-01-01", tz="UTC"), end_date=pd.Timestamp("2011-11-07 01:00", tz="UTC"))
online = pd.DataFrame(store.get_online_features(features=FEATURES, entity_rows=[{"customer_id": int(c)} for c in test_rows.customer_id]).to_dict())
served = test_rows.merge(online, on="customer_id")
offline_view = store.get_historical_features(entity_df=test_rows, features=FEATURES).to_df()
check = served.merge(offline_view, on="customer_id", suffixes=("_online", "_offline"))
same = all(np.allclose(check[f"{n}_online"], check[f"{n}_offline"]) for n in NAMES)
print(f"online features match the offline store for all {len(check):,} test customers: {same}")
print(f"\n{'training data':<26} {'validation AUC, October':>24} {'AUC in November, live features':>31}")
for name, table in (("naive join, latest values", naive), ("point-in-time join", point_in_time)):
october = table.event_timestamp == pd.Timestamp("2011-10-03", tz="UTC")
model = HistGradientBoostingClassifier(random_state=0).fit(table.loc[~october, NAMES], table.loc[~october, "bought"])
validation = roc_auc_score(table.loc[october, "bought"], model.predict_proba(table.loc[october, NAMES])[:, 1])
live = roc_auc_score(served.bought, model.predict_proba(served[NAMES])[:, 1])
print(f"{name:<26} {validation:>24.3f} {live:>31.3f}")offline store: 134,401 rows (49 weekly snapshots); training rows: 20,411 from 7 monthly cut-offs; November test rows: 4,042
training rows returned: point-in-time 20,411, naive join 20,411
online features match the offline store for all 4,042 test customers: True
training data validation AUC, October AUC in November, live features
naive join, latest values 0.798 0.716
point-in-time join 0.727 0.738What the output shows
- The leak does not show in row counts. Both training sets had 20,411 rows.
- The naive join looked better in validation, with an AUC of 0.798 against 0.727. Its features came from the final week of data, after the October cut-off, so they described the very purchases the label was asking about.
- In production it was worse. With features served from the online store as they stood on 7 November, the naive model scored 0.716, while the point-in-time model reached 0.738, close to its own validation figure. Honest validation was lower, and it was the one that held.
- Training and serving agreed. For all 4,042 test customers, online values matched the offline store, because one definition produced both. That consistency is the other half of the job.
When a team needs one
- Several models reuse the same features, and duplicated pipelines are drifting apart.
- Predictions happen in real time and need features in milliseconds.
- Building point-in-time training sets by hand keeps going wrong.
- Several teams need to discover and share features.
A single batch model fed by one SQL query rarely needs one. When you do, Feast is the main open-source option, and the major clouds offer managed versions; Amazon SageMaker's version, for example, provides an online store for real-time inference and an offline store for training (AWS: SageMaker documentation).
For computing fresh features from streams, see real-time feature engineering. For designing the features themselves, see feature engineering with examples, and for validation that respects time, see cross-validation in machine learning.
Costs
- Another system to run, with jobs, storage and on-call.
- Freshness to guarantee: materialisation must keep up, and stale values need alerts.
- Ownership: every feature needs an owner and documentation, or the registry turns into a junk drawer.
Starting small
Begin with the features that more than one model needs, or that a live service needs within milliseconds. Define each once, with an owner, a description and a time-to-live that matches how fast it changes: a customer's lifetime order count can be a week old, while a count of card payments in the last hour cannot. Backfill enough history to build training sets, schedule materialisation, and alert when the newest online value is older than its time-to-live. Then move one model onto the store end to end, from training set to live lookups, before inviting other teams.
Common mistakes
- Joining current values to historical labels.
- Recomputing features differently in the serving code.
- No time-to-live, so stale values are served silently.
- A feature store with a single user.
- Not monitoring materialisation freshness.
What is a feature store in machine learning?
What is a point-in-time join?
Do I need a feature store?
What is the difference between the online and offline store?
Feature stores are part of the Advanced MLOps Concepts module of the MLOps course.
Sources
- Introduction (Feast documentation) — Feast
- Point-in-time joins (Feast documentation) — Feast
- Create, store, and share features (Amazon SageMaker AI documentation) — Amazon Web Services
- Online Retail dataset (UCI Machine Learning Repository) — UCI Machine Learning Repository
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master MLOps?
Join GloryTecks’ expert-led MLOps training in Hyderabad with real-time projects and 100% placement support.
Related articles
MLOps Roadmap 2026: Skills, Tools and Career Path
MLOps sits between data science, software engineering and operations, which makes it hard to know where to start. This roadmap orders the skills from foundations to production operations, gives each stage a project that proves the skill, and explains what the MLOps engineer role actually involves.
MLflow Tutorial: Tracking Experiments and Models
MLflow records what produced each model so you never have to remember. This tutorial trains three models on real bike-sharing data, logs every run, compares them by querying the tracking store, registers the best one, points a champion alias at it and loads it back by that alias, all on a laptop.
Docker for Machine Learning: A Practical Guide
How to containerise a machine-learning project so it runs the same everywhere: pinning the exact library versions a saved model depends on, a Dockerfile ordered for fast rebuilds, keeping data out of images, multi-stage builds, non-root users and GPU access.