Models are only as good as the features they are trained and served with, and keeping those two consistent is surprisingly hard. This page explains what a feature store does, the offline/online split, how point-in-time joins prevent leakage, and when one is worth adopting.
A feature is an input value a model uses, such as "number of orders in the last 30 days" for a customer. A feature store is a system that manages features end to end: it holds their definitions, computes or ingests their values, stores history for training, serves the latest values at low latency for inference, and records metadata so features can be found and reused.
The diagram shows the standard two-store design. The offline store (usually tables in a warehouse or lake) keeps every historical value with a timestamp; it is optimised for large scans to build training sets. The online store (a key-value database such as Redis, DynamoDB or Cassandra) keeps only the latest value per entity key and answers lookups in milliseconds. A materialisation job copies recent values from offline to online.
Three recurring problems motivate feature stores:
A feature store is not always needed. A single batch model scored nightly can be served from a warehouse table. The value appears with real-time inference, many models sharing features, or several teams.
Training data is a table of label events: entity key, timestamp, and outcome (for example, "customer 1, 9 September, did not churn"). For each row, the feature store finds, per feature, the most recent value whose timestamp is at or before the event timestamp. Formally, for label time t it selects the value with max(feature_ts) where feature_ts ≤ t, optionally ignoring values older than a time-to-live (TTL).
This is an "as-of" join. In SQL it can be written with a window function or a lateral join; dataframe libraries offer it directly (merge_asof in pandas, join_asof in Polars), and Spark-based stores implement it at scale. The diagram shows why it matters: a naive equality join on customer_id would attach the day-12 value to a day-9 label, leaking information the model will not have when predicting.
A second timing subtlety is availability time. If the day-5 value was computed by a batch job that only finished on day 6, it was not truly available on day 5. Careful stores track both the event timestamp and when the value was written.
Features differ in how they are computed, and a mature store supports all three kinds. Batch features are computed on a schedule from the warehouse (for example, lifetime spend recomputed nightly); they are cheap but can be up to a day old. Streaming features are updated from an event stream within seconds (for example, the number of failed logins in the last ten minutes) and are written to both stores as they change. On-demand features are computed at request time from inputs only available then, such as the distance between the user's current location and a merchant. The store's job in every case is the same: one definition, versioned in code, producing identical values whether it is called while building a training set or while answering a live request.
Freshness, cost and complexity rise in that order, so default to batch features and promote a feature to streaming only when an experiment shows the fresher value actually improves the model.
The heart of an offline store in a few lines of pandas:
# Point-in-time join with pandas.merge_asof: the core operation of an offline store.
import pandas as pd
features = pd.DataFrame({
"customer_id": [1, 1, 1, 2, 2],
"feature_ts": pd.to_datetime(["2026-09-01", "2026-09-05", "2026-09-12",
"2026-09-02", "2026-09-08"]),
"orders_30d": [2, 4, 9, 1, 3],
}).sort_values("feature_ts")
labels = pd.DataFrame({
"customer_id": [1, 2],
"event_ts": pd.to_datetime(["2026-09-09", "2026-09-09"]),
"churned": [0, 1],
}).sort_values("event_ts")
train = pd.merge_asof(labels, features,
left_on="event_ts", right_on="feature_ts",
by="customer_id", direction="backward", # only past values
tolerance=pd.Timedelta("30D")) # ignore stale values
print(train[["customer_id", "event_ts", "orders_30d", "churned"]])
# customer_id event_ts orders_30d churned
# 0 1 2026-09-09 4 0
# 1 2 2026-09-09 3 1
Customer 1's label on 9 September picks up orders_30d = 4 (the 5 September value), not 9 from 12 September. The same idea expressed with Feast, an open-source feature store, declares the entity, source and feature view once, then uses one call for training data and another for online lookups:
# Feast feature definition (feature_repo/features.py) - API as of Feast 0.3x+
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Int64
customer = Entity(name="customer", join_keys=["customer_id"])
source = FileSource(path="data/customer_stats.parquet", timestamp_field="feature_ts")
customer_stats = FeatureView(
name="customer_stats", entities=[customer], ttl=timedelta(days=30),
schema=[Field(name="orders_30d", dtype=Int64)], source=source)
# Training: store.get_historical_features(entity_df=labels, features=["customer_stats:orders_30d"])
# Serving: store.get_online_features(features=[...], entity_rows=[{"customer_id": 1}])
Feast's API changes between releases, so check the documentation for your version; the concepts (entity, feature view, source, TTL, historical vs online retrieval) are stable across tools.
| Option | Online serving | Point-in-time joins | Best for |
|---|---|---|---|
| Plain warehouse tables | No (batch only) | Hand-written SQL | Batch scoring, small teams |
| Feast (open source) | Yes, pluggable stores | Built in | Teams wanting control over infrastructure |
| Databricks Feature Engineering / Unity Catalog | Yes | Built in | Databricks-centric platforms |
| Amazon SageMaker Feature Store | Yes | Via offline store queries | AWS-centric platforms |
| Vertex AI Feature Store | Yes | Built in | GCP-centric platforms |
| Tecton (commercial) | Yes, streaming features | Built in | Real-time features at scale |
Choose by where your data and models already live; the integration cost usually outweighs feature differences.
merge_asof.