This is part one of three. It covers everything you need to put a feature store to work: what a feature store is for, why machine-learning teams keep reinventing one badly, and how to build a working Feast repository that serves the same features to a training script and to a live API. By the end you will have a feature repository on disk, a registry, an online store you can query in single-digit milliseconds, and a mental model precise enough that the error messages start making sense. Mid-level and Senior take the same material further into production: compute engines, SQL registries, RBAC, Kubernetes. Nothing here is throwaway.
Everything below was checked against Feast 0.66.0 (released 21 August 2026) on Python 3.10. Feast is still a pre-1.0 project, which has a practical consequence we will return to more than once: minor releases can and do change configuration keys, so you pin the version.
Each section ends with a Try it task. Do them as you go. Feature stores are one of those tools where the concepts stay abstract until you have watched your own get_online_features call come back full of None and worked out why.
What a feature store is, and the problem Feast solves
A feature is one input to a model: the number of trips a driver completed today, the average basket value of a customer over the last thirty days, whether an account has changed its phone number in the last hour. A feature store is the piece of infrastructure that computes, stores, catalogues and serves those values, so that the same definition is used everywhere the feature is needed.
That sounds like plumbing, and it is, but it exists to solve a specific and expensive failure. Consider a fraud-scoring model. During training, a data scientist writes a SQL query against the warehouse that computes "number of transactions by this card in the previous hour" across two years of history, joins it to a label column, and trains a model that scores 0.94 AUC in the notebook. The model goes to production. In production there is no warehouse query at request time, because the API has forty milliseconds to answer. So a backend engineer writes a second implementation: a Redis counter, incremented by a stream consumer, read at scoring time.
Those two implementations now have to agree exactly, forever, across two codebases, two languages and two teams. They do not. The warehouse query counted transactions in a one-hour window aligned to the clock; the Redis counter uses a rolling hour. The warehouse treated declined transactions as transactions; the stream consumer filters them out. Nobody notices, because nothing crashes. The model simply performs worse in production than it did offline, and the investigation takes six weeks.
This gap has a name: training/serving skew. It is the single most common reason a model that looked good offline underperforms online, and it is structural rather than careless. The moment you have two definitions of the same number, they will diverge.
Feast's answer is to make the feature definition a single declarative object, written once in Python, committed to git, from which both paths are served. The training path reads it through get_historical_features. The serving path reads it through get_online_features. Same name, same schema, same source of truth.
Feast solves three problems with that one idea, and it is worth separating them because teams usually arrive with only one of the three in mind.
Consistency. One definition, two retrieval paths, so skew becomes a bug you can fix rather than a drift you cannot see.
Correctness of historical data. Building a training set means answering, for every labelled example, "what did this feature look like at that moment, and not a minute later?" Doing that by hand in SQL is possible and is where most of the subtle bugs in applied machine learning live. Feast does it for you with a point-in-time correct join, which the next sections cover in detail, because it is the part of Feast that genuinely cannot be hand-waved.
Discovery and reuse. Features are expensive to build and almost always useful to more than one model. A registry that lists every feature, who owns it, which source it comes from and which models consume it turns that work into an asset instead of a notebook someone will lose.
- Pick a model you or your team has shipped, or any model you have read about.
- List five features it uses, and for each one write down where the training value came from and where the serving value came from.
- Mark every feature where those two answers are different systems.
What came before, and why Feast looks the way it does
It helps to know the shape of what Feast replaced, partly for context and partly because a lot of tutorials you will find online describe a Feast that no longer exists.
Before feature stores, the two implementations described above were the industry norm, usually with a third variant for batch scoring. Teams that outgrew that pain built internal platforms — Uber's Michelangelo is the famous published example — and those platforms converged on the same shape: a declarative feature registry, a batch path into a warehouse, and a low-latency path into a key-value store.
Feast was the first widely used open-source implementation of that shape. Its own history matters to you as a learner because Feast 0.9 and Feast 0.10+ are effectively different products. The old architecture had server-side components called Feast Core and Feast Spark, a Client class, and an object called a FeatureTable. All of that is gone. The current design is deliberately lighter: there is no always-on Feast service you must operate just to define features. The registry is a file or a database table, the SDK talks directly to your stores, and the only long-running process is an optional feature server you run if you want HTTP access.
The practical rule: if a tutorial mentions feast-core, FeatureTable, or a Client object, close the tab. The same goes for a handful of renamed API arguments that litter older blog posts. The table below is worth bookmarking, because these are the differences that produce confusing type errors rather than clean "unknown argument" failures.
| Old form in old tutorials | Current form in 0.66 |
|---|---|
FeatureView(features=[...]) |
FeatureView(schema=[Field(...)]) |
Feature(...) and ValueType.FLOAT |
Field(...) and Float32 from feast.types |
FeatureView(batch_source=...) |
FeatureView(source=...) |
Entity(value_type=...) |
Entity(join_keys=[...]) |
event_timestamp_column= on a source |
timestamp_field= |
FeatureTable, feast-core, Client |
removed; use FeatureStore and the registry |
The second piece of background is Feast's pre-1.0 release reality, because it changes how you should treat the version number. Feast ships a minor release roughly every four to six weeks and stays on 0.x. Of the ten releases before 0.66.0, exactly one carried an explicit breaking change: 0.65.0 renamed the configuration key total_timeout_ms to batch_total_timeout_ms. That is a reassuring ratio, but the lesson is still to pin.
pip install "feast==0.66.0"
Write that into requirements.txt with the ==, not a >=. A pre-1.0 project that renames a YAML key in a minor release will eventually rename one you depend on, and you want that to happen when you choose to upgrade rather than the next time CI rebuilds an environment.
- Open the Feast releases page on GitHub and read the notes for 0.65.0.
- Find the breaking-changes section and the renamed key.
- Read the notes for 0.66.0 and confirm there is no breaking-changes section.
The core nouns: entity, source, feature view, feature service
Feast is a small vocabulary. Learn four nouns properly and most of the API becomes guessable.
Entity. The thing your features describe: a driver, a customer, a merchant, a device. An entity's job is to declare the join key, the column name that identifies one of those things in your data.
from feast import Entity
driver = Entity(name="driver", join_keys=["driver_id"])
The name is how you refer to the entity in definitions; the join key is the actual column. They are often similar and it is tempting to treat them as the same thing, but keeping them distinct is what lets you attach several entities to one feature view. When you do that, each entity contributes its own join key and the view is keyed on the combination — the effect of a composite key, built out of simple parts.
Data source. A pointer to where values actually live, plus the name of the column that says when each row became true.
from feast import FileSource
driver_stats_source = FileSource(
name="driver_stats_source",
path="data/driver_stats.parquet",
timestamp_field="event_timestamp",
)
Feast ships many source types — FileSource, BigQuerySource, SnowflakeSource, RedshiftSource, KafkaSource, PushSource, RequestSource, RaySource, MlflowDatasetSource, Spark and Postgres sources among them. They differ in connection details and share the important part: timestamp_field. That column is not decoration. It is the mechanism by which Feast can answer historical questions correctly, and a source without a sensible event timestamp is a source Feast cannot reason about.
Feature view. The central object. A named group of typed features, drawn from one source, keyed by zero or more entities, with a time-to-live.
from datetime import timedelta
from feast import FeatureView, Field
from feast.types import Float32, Int64
driver_stats_fv = FeatureView(
name="driver_hourly_stats",
entities=[driver],
ttl=timedelta(days=1),
schema=[
Field(name="conv_rate", dtype=Float32),
Field(name="acc_rate", dtype=Float32),
Field(name="avg_daily_trips", dtype=Int64),
],
source=driver_stats_source,
)
Read that object as a sentence: the features called conv_rate, acc_rate and avg_daily_trips, which describe a driver, come from this parquet file, and a value stays valid for one day.
Two rules about feature views catch people out. First, a feature view can legitimately have no entities — useful for features that describe the whole system rather than one object, such as a global average. Second, feature view names must be unique across every view type within a project: you cannot have a regular feature view and an on-demand feature view with the same name.
Feature service. A named bundle of features that one model version consumes.
from feast import FeatureService
driver_activity_v1 = FeatureService(
name="driver_activity_v1",
features=[driver_stats_fv],
)
The official guidance here is specific and worth following from day one: one feature service per model version, named accordingly. The reason is that a model's input contract is exactly the list of features it was trained on, in the schema it was trained with. If the serving code asks for a feature service instead of enumerating feature names, then upgrading the model is a matter of pointing at driver_activity_v2, and the old contract keeps working for whatever is still running the old model. Feature views referenced by a feature service are intended to be immutable and not deleted, which is the same idea seen from the other side.
Why it matters: both retrieval paths read the same registered definition, which is the entire argument for using a feature store at all.
Three supporting nouns complete the picture. The registry is where applied definitions are stored — a local file by default, a database table in production. The offline store is the compute layer that answers historical queries; a Feast project has exactly one, though it may define many data sources. The online store is the low-latency key-value layer that serves single-entity lookups, and Feast supports a long list of them including SQLite, Redis, DynamoDB, PostgreSQL, Cassandra, Bigtable and several vector databases. Offline and online stores do not have to be in the same cloud, which matters if your warehouse is in one region and your serving cluster in another.
- Write the four nouns on paper: entity, data source, feature view, feature service.
- Model one feature from your own work with them, inventing the column names.
- Say out loud which of the four holds the TTL, and which holds the join key.
Point-in-time correctness, the idea that justifies the tool
If you take one technical concept away from this guide, take this one. It is what get_historical_features does, it is the hardest part to get right by hand, and it is a guaranteed interview question.
Suppose you are training a model to predict whether a driver will accept the next ride offer. Your labels look like this: at 14:03 on 3 June, driver 1001 was offered a ride and declined. At 09:47 on 4 June, driver 1002 accepted. Each labelled example is an (entity, timestamp, outcome) triple.
Now you want to add the feature avg_daily_trips. The naive approach is to join your label table to your feature table on driver_id and take the current value. That produces a model with an excellent offline score and no predictive power whatsoever, because the "current" value of avg_daily_trips for driver 1001 was computed after 3 June — it includes information about trips that had not happened yet when the decision was made. You have leaked the future into the past.
This is label leakage, and the correct fix is to join each label row to the most recent feature value whose event timestamp is at or before that row's timestamp. That is a point-in-time correct join, sometimes called an as-of join.
Point-in-time join
- Each label row gets the feature value as of its own timestamp
- Values newer than the label are invisible
- Values older than the TTL are treated as expired
- Offline score is an honest estimate of online behaviour
Naive join on the key
- Every label row gets today's feature value
- Future information leaks backwards
- Stale values are silently treated as fresh
- Offline score is inflated and meaningless
Feast does this join for you, which is why the input to get_historical_features is not a list of entity IDs but an entity dataframe: a table with your join-key columns and an event_timestamp column, one row per labelled example. You hand Feast the questions — "driver 1001 at 14:03 on 3 June" — and it returns the answers.
import pandas as pd
entity_df = pd.DataFrame({
"driver_id": [1001, 1002, 1003],
"event_timestamp": pd.to_datetime([
"2026-06-03 14:03:00",
"2026-06-04 09:47:00",
"2026-06-04 11:15:00",
], utc=True),
"accepted": [0, 1, 1],
})
Notice that the label column rides along untouched. Feast joins features onto whatever other columns you bring, which means the dataframe that comes back out is already the training frame.
This is also where TTL earns its keep. A TTL of one day on driver_hourly_stats tells Feast that a value whose event timestamp is more than a day older than the label row should be treated as missing rather than used. That is usually what you want: a conversion rate computed three weeks before the decision is not a fact about the decision. Set the TTL to reflect how long the feature actually stays meaningful, and expect nulls when your materialisation falls behind — those nulls are Feast being honest with you.
event_timestamp column in the entity dataframe is the most common first failure with get_historical_features, and the resulting ValueError is easy to misread as a problem with your feature view. Check the entity dataframe first. While you are there, make the timestamps timezone-aware — mixing naive and aware timestamps across the entity dataframe and the source produces comparison errors that look mysterious and are not.
- Take three labelled events with timestamps and one feature table with several values per key.
- By hand, write down which feature row each label row should receive.
- Now do it again assuming a TTL of one hour, and mark which label rows become null.
Installing Feast and checking the setup
Feast 0.66.0 requires Python 3.10 or newer. This is one of the few places the official docs are internally inconsistent: the Quickstart page still says "Python (3.9 or above)", while the package metadata for 0.66.0 requires 3.10. Trust the metadata — pip will simply refuse to install on 3.9.
Always install into a virtual environment. Feast pulls in a substantial dependency tree (pyarrow, pandas, protobuf, several store clients), and the version pins in that tree are exactly the kind of thing you do not want shared with the rest of your system.
python3 -m venv .venv
source .venv/bin/activate
pip install "feast==0.66.0"
feast version
On Windows, PowerShell:
py -3.10 -m venv .venv
.venv\Scripts\Activate.ps1
pip install "feast==0.66.0"
feast version
The base install gives you the SDK, the feast CLI, the feature server, SQLite as an online store and local file handling. Everything else arrives through extras, the bracketed names after the package. Guessing extras names is a waste of an afternoon, so here is the complete list published for 0.66.0:
aerospike, aws, azure, cassandra, clickhouse, couchbase, delta, docling, duckdb, elasticsearch, faiss, flink, gcp, ge, go, grpcio, remote, hazelcast, hbase, ibis, k8s, image, milvus, mongodb, mssql, oracle, mysql, openlineage, opentelemetry, spark, iceberg, trino, postgres, postgres-c, pytorch, qdrant, rag, ray, redis, scylladb, singlestore, snowflake, sqlite-vec, mcp, mlflow, dbt, test, ci, nlp, dev, docs, minimal, minimal-sdist-build, setuptools
Of those, dev, ci, test, docs, minimal-sdist-build and setuptools are for people contributing to Feast itself, not for users. The rest map to a store, an engine or an integration.
pip install "feast[redis]==0.66.0"
pip install "feast[gcp,snowflake]==0.66.0"
pip install "feast[spark,postgres]==0.66.0"
Quote the whole argument. In zsh — the default shell on macOS — square brackets are glob characters, and an unquoted feast[redis] will either be mangled or produce a "no matches found" error that has nothing to do with pip.
Verify the install before you write any definitions:
feast version
feast --help
The second command matters more than it looks. The official CLI reference page is incomplete: it documents apply, materialize, teardown, entities list and friends, but omits serve, serve_offline, serve_registry, ui and plan, all of which exist and are documented elsewhere on the site. When you need to know what a command accepts, feast <command> --help on the version you have installed is the authority.
- Create a virtualenv, install
feast==0.66.0, and runfeast version. - Run
feast --helpand compare the command list to the online CLI reference. - Find at least two commands that exist in your terminal but not on that page.
Your first feature repository, step by step
Feast scaffolds a complete, working project for you, and the scaffold is genuinely good teaching material. Start there rather than from an empty directory.
feast init my_project
You will see Creating a new Feast repository in <path>. and a small tree appears. The interesting part is one level down:
cd my_project/feature_repo
ls
You get four things: a data/ directory holding a parquet file of synthetic driver statistics, feature_definitions.py with entities, sources and views, feature_store.yaml with the configuration, and test_workflow.py, which exercises every important command end to end.
feast init also takes a template with -t, which changes what gets scaffolded — feast init -t gcp my_project for a BigQuery-backed project, or feast init -t rag my_project for the retrieval-augmented-generation template added in 0.66. For a first project, the default local template is the right choice, because it needs no cloud account and no credentials.
Open feature_store.yaml first. The whole local configuration is five lines:
project: my_project
registry: data/registry.db
provider: local
online_store:
type: sqlite
path: data/online_store.db
Four of those keys deserve a sentence each. project is the namespace; its name may contain only letters, numbers and underscores, and a hyphen will be rejected. registry points at the file that will hold your applied definitions. provider is the deployment environment — local, gcp or aws — which mostly decides default stores and where the registry is allowed to live. online_store configures the low-latency layer, here a SQLite file, which is perfect for learning and unusable for production precisely because a file cannot be written concurrently by several processes.
Notice what is absent: there is no offline_store block. The local provider defaults to reading your files directly, which is why a parquet file on disk works with no further configuration.
Now apply the definitions:
feast apply
Two things happen, and keeping them separate in your head prevents a lot of confusion later. First, Feast reads every Python file in the repository, collects the objects you defined, and writes them into the registry. Second, it provisions infrastructure that the configured stores need — for SQLite, creating tables; for DynamoDB, creating tables in AWS.
What feast apply does not do is move any data. Your online store is now correctly shaped and completely empty. This is the single most common source of "Feast is broken" reports from beginners, and we will hit it deliberately in a moment.
Confirm the registry has content:
feast entities list
feast feature-views list
Each prints a small table. If feature-views list is empty after a successful feast apply, your definitions file is not being picked up — usually because you are in the wrong directory, or because the objects are created inside a function rather than at module level.
python test_workflow.py in the scaffolded repo executes apply, historical retrieval, materialisation, online retrieval and teardown in sequence. Run it, watch it succeed, then read it top to bottom. It is about eighty lines and it is the most compact correct example of the whole Feast lifecycle you will find.
- Run
feast init my_projectandcd my_project/feature_repo. - Run
feast apply, thenfeast feature-views list. - Run
python test_workflow.pyand read the file afterwards.
Reading features for training
With definitions applied, build a training set. This is the offline path, and it runs in your process against the offline store.
import pandas as pd
from feast import FeatureStore
store = FeatureStore(repo_path=".")
entity_df = pd.DataFrame({
"driver_id": [1001, 1002, 1003],
"event_timestamp": pd.to_datetime(
["2021-04-12 10:59:42", "2021-04-12 08:12:10", "2021-04-12 16:40:26"],
utc=True,
),
"label": [0, 1, 1],
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:acc_rate",
"driver_hourly_stats:avg_daily_trips",
],
).to_df()
print(training_df.head())
Three details in that snippet are load-bearing.
FeatureStore(repo_path=".") tells the SDK where the repository is. Get this wrong and you get a registry-not-found error, which reads like a corruption problem and is almost always a working-directory problem. The CLI has the same need and solves it with the global -c/--chdir flag, so feast -c path/to/feature_repo apply works from anywhere.
Feature names are strings of the form "<feature_view>:<feature>". The colon is the separator between view and feature, and it is the only syntax for referring to a feature in a retrieval call.
.to_df() is what actually executes the query. get_historical_features returns a retrieval job, not a dataframe; nothing runs until you materialise the result. On a local file source that distinction is invisible, but on BigQuery or Snowflake it is the difference between building a query and paying for it.
Once you have a feature service, prefer it over a hand-written list:
training_df = store.get_historical_features(
entity_df=entity_df,
features=store.get_feature_service("driver_activity_v1"),
).to_df()
Now the training script and the serving code name the same contract, and adding a feature to the model is a one-line change in the definitions file rather than an edit in two repositories.
Two flags on this call are worth knowing early. full_feature_names=True returns columns named driver_hourly_stats__conv_rate instead of conv_rate, which you need as soon as two requested views contain a feature with the same name — without it, Feast raises a collision error rather than silently picking one. And 0.66 added an opt-in filter_by_created_timestamp cutoff, for the case where you need to exclude rows that were created after a given moment as well as rows that became true after it; that distinction matters for late-arriving data and is a mid-level topic.
- Run the script above and inspect
training_df. - Move one entity row's timestamp a year into the past and re-run.
- Re-run with
full_feature_names=Trueand compare the column names.
Materialising and reading features online
The online path answers a different question: "what is the latest value for driver 1001, right now, in under ten milliseconds?" No timestamps, no joins, one key lookup.
For that to work, the values have to be in the online store, and getting them there is materialisation: copying the latest value per entity from the offline store into the online store.
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")
That command materialises everything from wherever it last stopped up to the timestamp you give it, which is why it is the one you schedule. feast materialize START END is the explicit-window version, for backfills and for re-running a window you know went wrong.
Then read:
from feast import FeatureStore
store = FeatureStore(repo_path=".")
features = store.get_online_features(
features=[
"driver_hourly_stats:conv_rate",
"driver_hourly_stats:avg_daily_trips",
],
entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}],
).to_dict()
print(features)
entity_rows is a list of dictionaries keyed by join key, one per entity you want. There is no timestamp anywhere in the call, because the online store holds exactly one value per key: the latest one materialisation wrote.
feast materialize-incremental at all? feast apply does not move data. (2) Is the end timestamp you passed later than the newest event timestamp in your source? Materialising up to yesterday when your data ends today writes nothing. (3) Has the TTL expired? Sample datasets are often months old, so a one-day TTL means every value is already stale. (4) Is the driver_id you are asking for actually present in the source? A typo in a key returns nulls, not an error.
That last point is worth dwelling on, because it is a design choice rather than an oversight. A missing key in the online store is a normal operational condition — a brand-new user has no history — so Feast returns nulls and tells you why in the response metadata. The to_dict() output includes a statuses list alongside the values, and a status of PRESENT versus a status indicating a missing or outdated value is how you tell "this entity has no data" apart from "this feature is genuinely null". Serving code should read the statuses, not just the values.
There is a third write path that bypasses materialisation entirely. A push source lets you write a value straight into the online store, the offline store, or both, at the moment it is computed:
store.push("driver_stats_push_source", df, to=PushMode.ONLINE_AND_OFFLINE)
This is how stream features reach Feast: your Kafka or Spark consumer computes the aggregate and pushes it, rather than waiting for the next materialisation window. For a beginner project you will not need it, but knowing it exists explains how Feast handles features that must be fresher than a batch schedule allows.
- In a fresh
feast apply-ed repo, runread_online.pybefore materialising. - Observe the nulls, then run
feast materialize-incrementalwith a current UTC timestamp. - Run the script again and print the full
to_dict(), statuses included.
The everyday commands, grouped by what you are doing
The CLI is small. Grouped by intent rather than alphabetically, it is easy to hold in your head. Remember that -c, --chdir is global, so any of these can run against a repository elsewhere on disk.
Creating and registering.
| Command | What it does |
|---|---|
feast init <name> |
Scaffold a repository; -t <template> picks a template |
feast apply |
Register definitions in the registry and provision store infrastructure |
feast teardown |
Remove provisioned infrastructure and registered objects |
feast apply takes two escape hatches: --skip-source-validation and --skip-feature-view-validation, both off by default. Validation is on for a reason — it catches a misspelled column before it becomes a null column — so reach for these only when you know the source legitimately does not exist yet, for example when a view points at a table a downstream job will create.
feast teardown deletes things. On a local SQLite project that is free; pointed at a shared registry it is destructive. Treat it as a development command.
Inspecting what is registered.
| Command | What it does |
|---|---|
feast entities list |
List entities; --tags filters, and all given tags must match |
feast feature-views list |
List feature views; --tags filters the same way |
feast registry-dump |
Print the whole registry contents |
feast configuration |
Show the resolved configuration |
feast delete <OBJECT_NAME> |
Delete the first object matching the name |
registry-dump is the debugging command people discover too late. When a feature name is not resolving, it tells you what Feast actually believes exists, as opposed to what your Python file says. Also note that feast delete removes "the first object matching the name" — it is not a precision instrument.
Moving data.
| Command | What it does |
|---|---|
feast materialize START END |
Materialise an explicit window; -v limits to one feature view |
feast materialize-incremental END |
Materialise from the last run up to END |
The -v <feature_view> flag on materialize is how you re-run one view without reprocessing everything, which on a warehouse-backed project is the difference between a minute and an hour.
Serving and browsing.
| Command | What it does |
|---|---|
feast serve |
Start the REST feature server |
feast ui |
Start the experimental web UI |
feast version |
Print the installed version |
These last two are among the commands missing from the official CLI reference page, so use --help for their flags.
Tags deserve a note, because they look cosmetic and are not. Any Feast object can carry key-value tags, and they drive three things: discovery in the UI, filtering in the CLI, and — at senior level — the matching rules for role-based access control. Tagging an object with its owning team on day one costs nothing and is what makes the registry navigable at fifty feature views.
driver_stats_fv = FeatureView(
name="driver_hourly_stats",
entities=[driver],
ttl=timedelta(days=1),
schema=[Field(name="conv_rate", dtype=Float32)],
source=driver_stats_source,
owner="fraud-team@example.com",
tags={"team": "fraud", "domain": "driver"},
)
- Add
ownerandtagsto a feature view and runfeast apply. - Run
feast feature-views list --tags team=fraud. - Run
feast registry-dumpand find your tags in the output.
Configuring feature_store.yaml
Configuration is one file, and the reference lists exactly seven top-level keys: project, provider, registry, online_store, offline_store, engine and materialization. Everything else is nested inside those.
project: driver_ranking
provider: local
registry: data/registry.db
online_store:
type: sqlite
path: data/online_store.db
The three you will change first are online_store, registry and offline_store.
Moving the online store off SQLite is the first real step towards production. The type key selects the integration and the remaining keys are that integration's connection details:
project: driver_ranking
provider: local
registry: data/registry.db
online_store:
type: redis
connection_string: ${REDIS_CONNECTION_STRING}
That ${REDIS_CONNECTION_STRING} is not pseudocode. Feast performs environment-variable substitution in feature_store.yaml, and it is the documented mechanism for keeping credentials out of git. Learn it now rather than committing a password and learning it from a security review. The pattern generalises: any value in this file can come from the environment, so the committed file describes shape and the environment supplies secrets.
The registry starts as a file and should not stay one in production. A file-based registry rewrites the entire file on every change, which means two concurrent writers can lose each other's work; the documentation actively discourages it for production in favour of the SQL registry. You do not need that yet, but when you read that recommendation, this is the reason.
The offline store needs configuring as soon as your data lives somewhere other than local files. A Feast project has exactly one offline store, and it must be able to query every data source you define — which is the real constraint behind the "one offline store" rule. Core options include Dask, DuckDB, BigQuery, Snowflake, Redshift, a remote offline server and, new in 0.66, a hybrid type; a longer contributed list adds Spark, PostgreSQL, Trino, Athena, ClickHouse, Ray and others.
Two nested keys under materialization are documented with their defaults and are worth knowing because they control memory and cost: online_write_batch_size defaults to null, and pull_latest_features defaults to false. Setting the latter to true pulls only the newest value per entity, which is usually all materialisation needs and can cut warehouse work substantially.
feature_store.yaml for local development and a separate file per deployed environment, differing only in store endpoints and registry location, with every credential coming from ${ENV_VAR}. The alternative — one file with commented-out production blocks — eventually gets applied with the wrong block uncommented.
- Add a harmless key to
feature_store.yamlusing${SOME_VAR}and export the variable. - Run
feast configurationand confirm the value was substituted. - Unset the variable and run it again.
Serving features over HTTP
The Python SDK is in-process: your serving code imports Feast and talks to the online store directly. That is the lowest-latency option and it is a perfectly good production design. But often the service that needs features is not Python, or you want feature access behind a network boundary. For that, Feast ships a stateless REST server.
feast serve
It listens on port 6566 by default, with one worker, and exposes interactive API docs at /docs. Useful defaults to know: --workers/-w is 1 and accepts -1 to calculate from the core count, --registry_ttl_sec/-r is 60 seconds, and --metrics is off. TLS comes from --key and --cert.
Reading features is a POST:
curl -X POST "http://localhost:6566/get-online-features" -d '{
"features": ["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"],
"entities": {"driver_id": [1001, 1002, 1003]}
}' | jq
Note the shape change from the SDK. In Python you pass entity_rows, a list of dictionaries. Over HTTP you pass entities, a dictionary of columns: one key per join key, each holding a list. This is a genuine difference and a common source of 422 responses when people transcribe the SDK call into curl. To use a feature service instead, replace features with "feature_service": "driver_activity_v1".
The response is column-oriented too: a metadata object with feature_names, and a results list where each element carries values, statuses and event_timestamps. The statuses are the same mechanism discussed earlier, and over HTTP they are the only way to distinguish a missing entity from a null feature.
Writing is also available. POST /push takes a push source name, a dataframe expressed as columns, and a to field accepting "online", "offline" or "online_and_offline":
curl -X POST "http://localhost:6566/push" -d '{
"push_source_name": "driver_stats_push_source",
"df": {
"driver_id": [1001],
"event_timestamp": ["2022-05-13 10:59:42+00:00"],
"created": ["2022-05-13 10:59:42"],
"conv_rate": [1.0], "acc_rate": [1.0], "avg_daily_trips": [1000]
},
"to": "online_and_offline"
}' | jq
Timestamps in that body must be strings, and they may need to be timezone-aware to match your offline store's schema. There is also POST /materialize and POST /materialize-incremental, so a scheduler can trigger materialisation over HTTP instead of shelling out to the CLI.
One deprecation to note because you will meet it in older examples: POST /retrieve-online-documents is now a deprecated alias of POST /search, with the same request and response shape. Write /search.
Three sibling servers round out the set, each for a different protocol: feast serve_offline exposes the offline store over Arrow Flight, feast serve_registry exposes the registry over gRPC, and feast ui serves the web catalogue. You will not need them on a laptop, but they are how a multi-team deployment stops every consumer from needing direct warehouse credentials.
- Run
feast servein one terminal and openhttp://localhost:6566/docs. - Reproduce your SDK call with the curl command above, noting the
entitiesshape. - Deliberately send
entity_rowsinstead and read the error.
The errors you will actually hit, and how to read them
Feast's failures cluster into a handful of families. Recognising the family is most of the diagnosis.
feast: command not found. The virtualenv is not activated. Run source .venv/bin/activate and try again. If feast version works in one terminal and not another, this is always the answer.
ModuleNotFoundError: No module named 'redis' or similar for psycopg, snowflake and friends. You configured a store whose extra you did not install. Install feast[redis] and remember that extras are not retroactive: changing type: in the YAML does not install anything.
Registry not found. Your repo_path or working directory is wrong. The registry is a path relative to the repository, so running a script from the project root when the repo is in feature_repo/ fails. Pass repo_path="feature_repo" or use feast -c feature_repo on the CLI.
A project-name validation error. Project names may contain only letters, numbers and underscores. driver-ranking is invalid; driver_ranking is fine.
A feature-name collision. Requesting two views that each contain a feature called conv_rate is ambiguous, and Feast raises rather than guessing. Pass full_feature_names=True and the columns come back prefixed with the view name.
FeatureViewNotFoundException. The registry does not contain the view you asked for. Either you have not run feast apply since adding it, or you are pointed at a different registry, or someone deleted it — the registry documentation confirms that fetching a deleted feature view raises exactly this. feast feature-views list settles it in one command.
A missing event_timestamp column in the entity dataframe. Covered earlier; the error surfaces as a ValueError from the retrieval path and the fix is always in your dataframe, not your definitions.
Nulls everywhere online. Work the four-step checklist from the materialisation section. The order matters: check that materialisation ran, then the end timestamp, then the TTL, then the key.
Two documented behaviours look like bugs and are not, so they are worth knowing before they cost you a debugging session. First, empty list values come back as None: write [[1, 2], [], [3]] and read back [[1, 2], None, [3]]. That is the type system's documented behaviour. Second, Redshift has no list type at all, so list-valued features and embeddings must be serialised to strings — JSON or protobuf — if your offline store is Redshift.
Finally, one trap that is destructive rather than merely confusing. Feast serialises entity keys, and version 2 of that serialisation is deprecated in favour of version 3. The documentation is blunt about the consequence: a version 2 serialised entity key cannot be retrieved using the version 3 deserialisation algorithm. If you flip entity_key_serialization_version on a populated online store, your reads start returning nothing, with no error. The documented migration is to reserialise the existing keys — enumerate the views, read the keys out, convert them with the helper in Feast's key_encoding_utils, and write them back — not to change the flag and hope. There is no official migration script.
- Cause three errors on purpose: deactivate the venv and run
feast apply; rename your project to use a hyphen; drop theevent_timestampcolumn from an entity dataframe. - Write down the first line of each message.
- Fix each one and note which part of the message pointed at the fix.
Putting it all together
Here is the whole lifecycle as one small project: a driver-ranking model with a training set, an online lookup and a feature service, built from the scaffold.
python3 -m venv .venv && source .venv/bin/activate
pip install "feast==0.66.0"
feast init driver_ranking
cd driver_ranking/feature_repo
Replace the definitions file with something you wrote yourself, keeping the scaffold's parquet file as the source:
from datetime import timedelta
from feast import Entity, FeatureService, FeatureView, Field, FileSource
from feast.types import Float32, Int64
driver = Entity(
name="driver",
join_keys=["driver_id"],
description="A driver on the platform",
)
driver_stats_source = FileSource(
name="driver_stats_source",
path="data/driver_stats.parquet",
timestamp_field="event_timestamp",
created_timestamp_column="created",
)
driver_hourly_stats = FeatureView(
name="driver_hourly_stats",
entities=[driver],
ttl=timedelta(days=3),
schema=[
Field(name="conv_rate", dtype=Float32),
Field(name="acc_rate", dtype=Float32),
Field(name="avg_daily_trips", dtype=Int64),
],
source=driver_stats_source,
owner="ml-platform@example.com",
tags={"team": "marketplace", "domain": "driver"},
)
driver_ranking_v1 = FeatureService(
name="driver_ranking_v1",
features=[driver_hourly_stats],
)
Register it, then move data into the online store:
feast apply
feast feature-views list
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")
Build the training set through the feature service, so the training contract and the serving contract are the same object:
import pandas as pd
from feast import FeatureStore
store = FeatureStore(repo_path=".")
entity_df = pd.DataFrame({
"driver_id": [1001, 1002, 1003, 1004],
"event_timestamp": pd.to_datetime(
[
"2021-04-12 10:59:42",
"2021-04-12 08:12:10",
"2021-04-12 16:40:26",
"2021-04-12 15:01:12",
],
utc=True,
),
"completed": [1, 0, 1, 1],
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=store.get_feature_service("driver_ranking_v1"),
).to_df()
print(training_df.columns.tolist())
print(training_df.isna().sum())
Printing the null counts is not decoration. It is the check that tells you whether your TTL and your entity timestamps are compatible, and it is the difference between training on a half-empty frame and noticing.
Now the serving side, the same contract:
from feast import FeatureStore
store = FeatureStore(repo_path=".")
def features_for(driver_id: int) -> dict:
response = store.get_online_features(
features=store.get_feature_service("driver_ranking_v1"),
entity_rows=[{"driver_id": driver_id}],
).to_dict()
return response
if __name__ == "__main__":
print(features_for(1001))
Finally, expose it over HTTP and confirm the two paths agree:
feast serve &
curl -X POST "http://localhost:6566/get-online-features" -d '{
"feature_service": "driver_ranking_v1",
"entities": {"driver_id": [1001]}
}' | jq
Compare that JSON to the output of score.py. Same features, same values, three interfaces — training, SDK serving, HTTP serving — and one definition in git. That is the entire argument for a feature store, demonstrated on a laptop in about twenty minutes.
Clean up when you are done:
feast teardown
- Build the project above end to end.
- Add a fourth feature to the view, re-apply, re-materialise, and confirm it appears in both paths without touching
train.pyorscore.py. - Change the TTL to one hour, re-run
train.py, and explain the null counts.
What you can now do, and what comes next
You can scaffold a Feast repository, define entities, sources, feature views and feature services, register them, build a point-in-time-correct training set, materialise values into an online store, read them from Python and over HTTP, configure the stores through feature_store.yaml with secrets kept in the environment, and recognise the handful of error families that account for most beginner time lost.
More importantly, you have the vocabulary. The next layer of Feast is mostly about replacing the local components with real ones, and every one of those replacements is a value change in the same seven-key configuration file you already understand.
What to learn next, in the order that pays off fastest:
Replace SQLite with a real online store. Redis, DynamoDB or PostgreSQL, configured with ${ENV_VAR} credentials. This is the smallest change that makes a project deployable and it teaches you what the extras system is for.
Replace the file registry with the SQL registry. The moment two people or two CI jobs apply to the same registry, the file-based one is a liability.
Schedule materialisation properly. materialize-incremental from cron is fine for one job and stops being fine quickly. The documented production pattern uses an orchestrator, so the Airflow guide or the Prefect guide is the natural next stop.
Learn one compute engine. Materialisation defaults to a single in-process implementation that does not scale. Swapping in the Spark engine is the usual first step, which makes the Spark guide relevant.
Connect Feast to the rest of your stack. Features feed models, and models need tracking and serving: MLflow for experiments and the model registry, BentoML for packaging the service that calls get_online_features, and Evidently or Great Expectations for watching whether the features themselves are still trustworthy. Feast also integrates with MLflow directly, both as an offline source and through its Kubernetes operator.
Then read the Mid-level guide. It covers on-demand and stream feature views, the compute-engine interface, entity-less retrieval and its store restrictions, the feature server's tuning knobs, and the production topologies. The Senior guide takes on the registry cache, RBAC and the Permission object, multi-team partial= semantics, the Kubernetes operator, and what breaks first at scale.
One regional note worth carrying into a job interview. A feature store makes the data-residency conversation easier rather than harder, because it separates the question of where features are computed from where they are served. Feast's offline and online stores can sit in different clouds and different regions, so a team in the Gulf or in Egypt can keep a warehouse in a compliant region while serving from a low-latency store close to the application — and the feature definitions, which are just Python in git, do not change. Being able to explain that split is a genuinely useful answer when a hiring manager asks how you would deploy a feature store under local data rules.
- Run a Redis container locally, install
feast[redis], and point your project's online store at it with a${REDIS_CONNECTION_STRING}. - Re-apply, re-materialise, and confirm your online reads still work unchanged.
- Write down every file you had to edit.
feature_store.yaml. Your definitions and both retrieval scripts were untouched. That separation is what the rest of the series builds on.
Sources
- Feast documentation
- Quickstart
- Concepts
- Feature views
- Architecture
- Registry
- Compute engine
- feature_store.yaml reference
- Feast CLI commands
- Python feature server
- Online stores
- Offline stores
- Entity key serialization v2 to v3
- Running Feast in production
- Scaling Feast
- FAQ
- Feast releases on GitHub
- Feast 0.66.0 release notes
- feast on PyPI