Skip to main content

Feature Store Guide

The Feature Store is where you define the features your models use, keep their values fresh, and serve them consistently for both training and live inference. It solves the two problems that quietly break most ML projects: the same feature being computed differently in training than in production (training/serving skew), and training data that accidentally includes information from the future (label leakage).

Open it from MLOps > Feature Store. The list page shows each store with its status, whether online serving is enabled, and the storage it is bound to. Use Create Feature Store in the page header to add one.

Concepts​

A feature store is built from four kinds of objects:

  • Entity: the thing features describe, identified by one or more join keys. For example, a driver entity keyed by driver_id.
  • Feature view: a named group of features for an entity, with a schema and a data source. A view is immutable per version: an additive change (a new nullable column) reuses the version, while a breaking change creates a new version so existing consumers keep resolving what they were built against.
  • Feature service: the model-facing contract. A service pins a specific set of features (at specific view versions) so a model always trains and serves against exactly the same definition.
  • Materialization: the scheduled refresh that moves feature values from the offline store into the online store so they can be served with low latency.

Storage​

A feature store keeps its definitions in the platform and its feature values in storage you select when you create it:

  • Offline store (required): a durable Postgres add-on or Postgres data source. It holds the full history of feature values and is the system of record for point-in-time training data.
  • Online store (optional): a Redis add-on or Redis data source. It holds the latest value per entity for low-latency serving. Enable it only when you need real-time features, online serving, or windowed aggregations; a store without it is training-only.

You choose these from your available add-ons and data sources when creating the store, the same way every other feature on the platform selects services. If you do not have a suitable add-on yet, create one first from Add-ons.

Creating a feature store​

  1. Go to MLOps > Feature Store and click Create Feature Store.
  2. Give it a name (lowercase letters, numbers, and underscores) and an optional description.
  3. Select the offline Postgres add-on or data source.
  4. To serve features in real time, turn on online serving and select the Redis add-on or data source.
  5. Click Create Feature Store. The store opens to its details page.

A new store is private to you. To share it, open its Permissions tab: share it with specific users, or allow all users in your organization. Someone a store is shared with for use sees its definitions and data read-only; only its owner and its editors change it.

Deleting a store, disabling it, or turning off online serving is blocked while a deployed model resolves online features from it. The platform tells you which models are using it so you can unbind them first.

Defining features​

You define entities, feature views, and feature services with the Python SDK or the REST API using a single apply call, which registers them in dependency order (entities, then views, then services). The result reports each view's change: created for a new view, identical, additive or breaking. Applying the same definition again is safe: an unchanged view is a no-op, an additive change updates in place, and a breaking change creates a new version.

from strongly import Strongly

client = Strongly()
store_id = client.feature_store.list_stores()[0]["_id"]

client.feature_store.apply(
store_id,
entities=[
{"name": "driver", "joinKeys": ["driver_id"], "valueType": "int64"},
],
views=[
{
"name": "driver_stats",
"entities": ["driver"],
"mode": "batch",
"ttl": "2d",
"schema": [
{"name": "conv_rate", "dtype": "float64"},
{"name": "acc_rate", "dtype": "float64"},
],
"source": {
"serviceKind": "addon",
"serviceId": "<your-postgres-addon-id>",
"type": "postgres",
"table": "driver_stats_raw",
"timestampField": "event_timestamp",
"entityKeyColumns": {"driver_id": "driver_id"},
},
},
],
services=[
{
"name": "driver_model_v1",
"features": [
{"view": "driver_stats", "feature": "conv_rate"},
{"view": "driver_stats", "feature": "acc_rate"},
],
},
],
)

Feature views and services appear on the store details page under their tabs, each showing versions, feature counts, and freshness.

Streaming views with windowed aggregations​

A streaming view aggregates events as they arrive: each aggregation is a column, a function (count, sum, min, max, avg, var, std, first, last, or the approximate approx_distinct, approx_quantile and top_k; the last two take a param, the quantile or the number of top values) and the windows to serve it over, each producing a feature named <column>_<function>_<window> (for example fare_sum_1h). Events are grouped into tiles of tileSize; allowedLateness is how late an event may arrive and still count online.

client.feature_store.apply(store_id, views=[{
"name": "driver_trips",
"entities": ["driver"],
"mode": "stream",
"schema": [{"name": "fare", "dtype": "float64"}],
"source": {"type": "rabbitmq", "exchange": "fs.driver_trips", "timestampField": "event_timestamp",
"entityKeyColumns": {"driver_id": "driver_id"}},
"aggregations": [{"column": "fare", "function": "sum", "windows": ["1h", "1d"]}],
"tileSize": "5m",
}])

client.feature_store.push(store_id, "driver_trips", events=[
{"driver_id": 1001, "event_timestamp": "2026-10-08T07:00:00Z", "fare": 12.5},
])

Online, a window is served as of now: an event older than a window no longer counts in it, also for an entity that sends no new events, so online values match training data computed for the same moment. Events pushed right after the view is applied are kept until the stream worker starts on it.

On-demand views​

An on-demand view computes its features when they are requested, from the features of other views and from values the request supplies, with your Python. Its transformCode defines transform(df): it receives a pandas DataFrame with one row per entity, holding every feature of its source views (None where the entity has no value) and its request fields, and returns a DataFrame with the same number of rows and a column for each of the view's features. Import what the code uses.

client.feature_store.apply(store_id, views=[{
"name": "conv_x_dist",
"entities": ["driver"],
"mode": "on_demand",
"schema": [{"name": "conv_x_dist", "dtype": "float64"}],
"onDemandSources": ["driver_stats"],
"requestDataSources": [{"name": "trip", "fields": [{"name": "trip_distance", "dtype": "float64"}]}],
"transformCode": (
"import pandas as pd\n"
"def transform(df):\n"
" return pd.DataFrame({'conv_x_dist': df['conv_rate'] * df['trip_distance']})\n"
),
}])

Applying it pins the newest active version of each source view, so a model serving it keeps the inputs it was trained on, and deploys its transform: a workflow in your organization, owned by you, in which your code runs in a sandbox, kept running to serve requests. The view's page shows the transform's state (provisioning, ready, or failed with the reason; applying the view again retries a failed one) and links to its runs. Any change to an on-demand view creates a new version with its own transform, and deleting a version removes its transform. A view version that an on-demand view reads cannot be deleted while it does.

Each request supplies the request fields with the entity: online, on each entity row ({"driver_id": 1001, "trip_distance": 3.0}); in training data, as columns of the entity rows.

Defining features in the UI​

On the store's page, the Entities tab adds an entity (its name, value type and join keys), the Feature Views tab adds a batch feature view, and the Feature Services tab bundles features from views into a service. A batch view names its features with their types, its entities, an optional TTL, and its Postgres source: the add-on or data source, the table, the event-time column, optionally an ingestion-time column (so a row that arrives late is still taken), and the source column holding each join key. Streaming and on-demand views carry transform code, so they are defined with the SDK (see On-demand views).

A feature view's page​

Click a view's name to open its page. It shows the newest version's definition (mode, entities, TTL, features and their types, windowed aggregations, the source, an on-demand view's inputs and transform), its data (freshness, rows and entities on record, each feature's share of empty and zero values, source ingestion and materialization with their last error), the feature services that serve it, and every version.

  • Materialize now pulls the rows the source gained and publishes the latest values to the online store, without waiting for the schedule. It is available when the store is ready with online serving on.
  • Edit changes a batch view. Adding a feature updates the version in place; removing or retyping a feature, or changing the source or entities, creates a new version and keeps the earlier one. A new version takes the source's rows from the beginning, since its history starts empty. The earlier version is kept as it was for the services pinned to it: it is no longer pulled or materialized, and the Monitoring tab shows it as kept, with the freshness and data of the newest version only. Move a service onto the new version by choosing the view again in the service's Edit.
  • Deprecate on an older version makes it read-only and hides it from discovery; the feature services pinned to it still resolve it. A version can also be deleted when nothing uses it.

A feature service's page​

Click a service's name on the Feature Services tab to open its page: the features its newest version serves, each with the view version it pins (flagged when that version is deprecated or deleted), the registered models trained on it, and every version. Edit changes its features: each feature keeps the view version it pins unless you choose its view again, which pins that view's newest version. Saving a change creates a new version of the service; models trained on an earlier version keep resolving it. Delete first shows what uses the service.

Finding features to reuse​

Feature Catalog on the Feature Store page lists every feature of the active views in the stores you can use. Search by name, or narrow by store, entity or type; each feature links to its view and shows its store, the view's mode, freshness, and its share of empty and zero values. Deprecated versions are not listed.

Ingesting and refreshing values​

Write feature rows into a view's history, then materialize the view to publish the latest values to the online store:

client.feature_store.write(store_id, "driver_stats", rows=[
{"driver_id": 1001, "event_timestamp": "2026-07-30T00:00:00Z", "conv_rate": 0.75, "acc_rate": 0.9},
])

client.feature_store.materialize(store_id, "driver_stats")

A batch view built over a source table needs no write: materializing it pulls the source's new rows into the history first, and the source is also pulled every five minutes. Materialization is incremental: it publishes only what changed since the last run and never moves an older value over a newer one. For streaming views, values update continuously as events arrive, and windowed aggregations (for example a 24-hour sum) are kept current automatically.

Serving features online​

Once a view is materialized, resolve the latest features for a set of entities. You send only the entity keys and receive the current feature values:

online = client.feature_store.get_online_features(
store_id, "driver_model_v1", entity_rows=[{"driver_id": 1001}]
)
print(online["results"])

Each result reports the resolved features and lists any that were missing (an entity with no value yet), so absent data is always visible and never silently filled in. You can set a maximum staleness so a value older than your tolerance is reported as missing rather than served stale.

Generating training data​

To train, ask for point-in-time correct features for a set of labeled examples. For each row, the feature store resolves each feature as of that row's event_timestamp, so a training set never contains information that was not yet known at that moment:

training = client.feature_store.get_historical_features(
store_id,
"driver_model_v1",
entity_rows=[
{"driver_id": 1001, "event_timestamp": "2026-07-30T12:00:00Z", "label": 1},
{"driver_id": 1002, "event_timestamp": "2026-07-30T12:00:00Z", "label": 0},
],
)
print(training["snapshot_url"], training["num_rows"])

The training set is delivered as a Parquet snapshot you read directly. For large label sets, point at a Parquet or CSV file in your storage instead of listing the rows inline:

training = client.feature_store.get_historical_features(
store_id, "driver_model_v1",
entity_s3_uri="s3://your-bucket/labels/train.parquet",
)

Training a model from a feature service​

AutoML can train directly on a feature service, so you never assemble the feature matrix by hand:

  1. Go to MLOps > AutoML and click Train New Model.
  2. For the dataset source, choose Feature Service.
  3. Select the feature store and the feature service to train against.
  4. Upload the entity and label file: a CSV or Parquet frame of entity join keys, an event_timestamp per row, and your target column.
  5. Choose the target column and continue through the wizard as usual.

AutoML resolves point-in-time features for your labels, joins them to the target, and trains on the result. The features it trained on are recorded with the model.

Serving a model with live features​

A deployed model can resolve its features at inference, so callers send only entity ids, for example {"driver_id": 1002}.

A model trained on a feature service (an AutoML job trained from one) is bound automatically: its Features tab shows Bound from training, naming the store and the feature service version it trained on, and each prediction resolves exactly those features from that version. A newer version of the service is used only by a model retrained on it, and a registry version or rollback serves the features its own version was trained on. You can set a Max staleness there: a prediction whose features are older fails instead of being served stale.

Any other model (one you uploaded, for example) is bound on its Features tab: select the feature store and the feature service, turn on Resolve online features at inference, optionally set a maximum staleness, and click Save binding. Remove binding undoes it. Online resolution requires the store to have online serving on.

A value the request carries for a feature is used instead of the resolved one. When the features cannot be resolved, the prediction fails rather than running the model without them: with 422 when the request cannot be served as sent (no entity keys, or an entity the store has no current value for), with the store's own error when it refuses the lookup (for example a missing join key), and with 502 when the store fails or cannot be reached.

Access and sharing​

Feature stores are owned by the user who creates them and follow the same sharing model as apps and other library items: keep a store private, share it with specific users, or make it readable across your organization. Every read and write is authorized against your access to the store, so discovering a store never implies permission to use it.