How to Set Up a Feast Feature Store with Redis and Postgres (2026)

Oct 31, 2026 · 13 min · Ajay Kumar

This guide is for engineers who need a working Feast feature store, not a demo notebook: by the end you will have a feature repo in Git, a point-in-time-correct training set from get_historical_features, features materialised into Redis (or Postgres), an HTTP feature server answering on port 6566, and a CI job that catches broken definitions before they reach the registry. I ran every command here on 31 October 2026 with Feast 0.67.0 (released 13 October 2026), Python 3.12.8, and Homebrew Redis 7.2 and PostgreSQL 16 on macOS. The Docker commands in Step 1 use the current image tags, which I checked exist but did not run; use redis:7.2 and postgres:16 if you want exactly what I tested. You need Python 3.10 or newer (PyPI) and either Docker or local Redis/Postgres. If you want the background on why feature stores have two databases and what point-in-time correctness buys you, read Feature Stores Explained (2026) first.

Step 1: Install Feast 0.67.0 and start Redis

Pin the version. Feast ships roughly monthly and feature repos are sensitive to SDK changes, so the version in CI, the materialisation job and the feature server image should always match.

python3 -m venv .venv && source .venv/bin/activate
pip install "feast[redis,postgres]==0.67.0"
feast version

Expected output:

Feast SDK Version: "0.67.0"

Start Redis and Postgres for local work. The Feast feature server image is published on Quay with the same version tag.

docker run -d --name feast-redis -p 6379:6379 redis:8.10.3
docker run -d --name feast-pg -p 5432:5432 \
  -e POSTGRES_USER=feast -e POSTGRES_PASSWORD=feast -e POSTGRES_DB=feast \
  -e TZ=UTC postgres:18.6
redis-cli -p 6379 ping   # PONG

Step 2: Create the repo and write feature_store.yaml

feast init generates a sample repo, but the 0.67 template includes label views, push sources and on-demand transforms, which is a lot to delete. I prefer a minimal layout:

payments-features/
  feature_repo/
    feature_store.yaml
    features.py
    .feastignore
    data/customer_stats.parquet
    ci/feature_store.yaml
    ci/test_features.py

feature_store.yaml tells Feast where the registry, offline store and online store live. This one uses Parquet files as the offline store and Redis online, matching the Redis online store docs:

# feature_repo/feature_store.yaml  (feast 0.67.0)
project: payments
provider: local
registry: data/registry.db          # file registry; fine for one writer
offline_store:
  type: file                        # Parquet; swap for bigquery, snowflake.offline, redshift, spark
online_store:
  type: redis
  connection_string: "localhost:6379"
entity_key_serialization_version: 3

For Redis Cluster, the docs use redis_type: redis_cluster with a comma-separated connection_string that can include ssl=true,password=.... Do not commit passwords: Feast expands ${ENV_VAR} references in this file, so keep secrets in the environment.

Step 3: Define entities, a data source and a feature view

An entity is the join key. A feature view binds a source table to a schema, an entity and a TTL. A feature service is a named, versioned bundle of features that a model consumes, and it is what you should reference from training and serving code so both ask for the same thing.

# feature_repo/features.py  (feast 0.67.0)
from datetime import timedelta

from feast import Entity, FeatureService, FeatureView, Field, FileSource
from feast.types import Float64, Int64
from feast.value_type import ValueType

customer = Entity(
    name="customer",
    join_keys=["customer_id"],
    value_type=ValueType.INT64,
    description="A paying customer",
)

customer_stats_source = FileSource(
    name="customer_stats_source",
    path="data/customer_stats.parquet",
    timestamp_field="event_timestamp",      # when the value became true
    created_timestamp_column="created",     # tie-breaker for late-arriving rows
)

customer_stats_fv = FeatureView(
    name="customer_stats",
    entities=[customer],
    ttl=timedelta(days=2),                  # lookback window for joins and freshness
    schema=[
        Field(name="txn_count_7d", dtype=Int64),
        Field(name="avg_amount_7d", dtype=Float64),
        Field(name="chargebacks_90d", dtype=Int64),
    ],
    online=True,
    source=customer_stats_source,
    tags={"owner": "risk-team"},
)

fraud_model_v1 = FeatureService(
    name="fraud_model_v1",
    features=[customer_stats_fv],
)

My Parquet file has one row per customer per day with customer_id, event_timestamp, created and the three feature columns, all timestamps in UTC.

Feast imports every .py file in the repo when you run feast apply, including helper scripts. Add a .feastignore (docs) for anything that is not a definition:

# feature_repo/.feastignore
ci
scripts

Step 4: Preview with feast plan, then feast apply

feast plan shows the registry and infrastructure diff without writing anything. feast apply registers the objects and creates online store tables or keys.

cd feature_repo
feast plan
feast apply

Output from my run:

Created project payments
Created entity customer
Created feature view customer_stats
Created feature service fraud_model_v1

Deploying infrastructure for customer_stats

Change something (I bumped the TTL to three days) and feast plan reports Updated feature view customer_stats followed by a protobuf diff. That diff is what you want reviewers to see on a pull request.

Step 5: Build a training set with get_historical_features

The entity dataframe is the spine of the training set: one row per label with the entity key and the timestamp at which the label was observed. Feast does a point-in-time join, finding the latest feature value at or before each row's timestamp within the TTL.

# train.py  (run from payments-features/)
from datetime import datetime, timezone

import pandas as pd
from feast import FeatureStore

store = FeatureStore(repo_path="feature_repo")

entity_df = pd.DataFrame({
    "customer_id": [1001, 1002, 1003],
    "event_timestamp": [
        datetime(2026, 10, 20, 12, 0, tzinfo=timezone.utc),
        datetime(2026, 10, 25, 9, 30, tzinfo=timezone.utc),
        datetime(2026, 10, 29, 18, 45, tzinfo=timezone.utc),
    ],
    "is_fraud": [0, 1, 0],   # labels pass through untouched
})

training_df = store.get_historical_features(
    entity_df=entity_df,
    features=store.get_feature_service("fraud_model_v1"),
).to_df()
print(training_df.to_string(index=False))

Output:

 customer_id           event_timestamp  is_fraud  txn_count_7d  avg_amount_7d  chargebacks_90d
        1001 2026-10-20 12:00:00+00:00         0            18          36.24                0
        1002 2026-10-25 09:30:00+00:00         1            24          35.12                2
        1003 2026-10-29 18:45:00+00:00         0            23         120.05                2

Customer 1001 had txn_count_7d = 18 on 20 October and 28 on 21 October. The join returned 18, the value that existed at label time. That single number is the reason to use a feature store instead of JOIN ON customer_id.

Step 6: Materialize features into the online store

Materialisation copies the newest value per entity from the offline store into Redis. Use an explicit window for the first backfill, then materialize-incremental on a schedule.

# one-off backfill (UTC timestamps)
feast materialize 2026-10-15T00:00:00 2026-10-31T00:00:00

# scheduled job: picks up from the last run
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")

redis-cli -p 6379 dbsize   # one hash per entity key: 3 in this example

Two behaviours to know. First, on a feature view's first incremental run Feast starts from now minus the TTL: in my run, with a two-day TTL, the window started at 2026-10-29 05:30:41+00:00. Anything older is not loaded, so do the explicit backfill first. Second, the Redis store keeps every feature view for one entity in a single hash, so reading several views for the same customer is one pipelined round trip (Redis store docs).

Run the scheduled job from whatever already runs your batch work (Airflow, a Kubernetes CronJob, GitHub Actions on a schedule) and alert when it fails or when the newest event_timestamp in the online store is older than you expect.

Step 7: Read online features from Python and the feature server

From Python:

from feast import FeatureStore

store = FeatureStore(repo_path="feature_repo")
print(store.get_online_features(
    features=store.get_feature_service("fraud_model_v1"),
    entity_rows=[{"customer_id": 1001}, {"customer_id": 9999}],
).to_dict())
{'customer_id': [1001, 9999], 'avg_amount_7d': [179.96, None], 'chargebacks_90d': [1, None], 'txn_count_7d': [37, None]}

Unknown keys come back as None, not an error, so your model code must handle missing features.

For non-Python services, run the Python feature server. It listens on 6566 by default:

feast serve --host 0.0.0.0 --port 6566
# production: feast serve --workers -1 --registry_ttl_sec 60 --metrics

curl -s -X POST http://localhost:6566/get-online-features \
  -H 'Content-Type: application/json' \
  -d '{"feature_service": "fraud_model_v1", "entities": {"customer_id": [1001, 1002]}}'

Trimmed response from my run:

{"metadata": {"feature_names": ["customer_id", "chargebacks_90d", "txn_count_7d", "avg_amount_7d"]},
 "results": [
   {"values": [1001, 1002], "statuses": ["PRESENT", "PRESENT"]},
   {"values": [1, 1], "statuses": ["PRESENT", "PRESENT"], "event_timestamps": ["2026-10-30T00:00:00Z", "2026-10-30T00:00:00Z"]},
   {"values": [37, 32], "statuses": ["PRESENT", "PRESENT"]},
   {"values": [179.96, 105.25], "statuses": ["PRESENT", "PRESENT"]}]}

Leave out the Content-Type header and 0.67 returns HTTP 422 with Input should be a valid dictionary, which is the first thing people hit. GET /health returns 200 for liveness probes. In containers, use quay.io/feastdev/feature-server:0.67.0, or the feast-feature-server Helm chart (chart version 0.67.0) in the Feast repo, with the same feature_store.yaml. --metrics exposes Prometheus metrics on port 8000.

Feast 0.67 in this guide: control path on top, data paths below features.py in Git entities, views, services CI: feast plan PR diff, PIT test feast apply on merge to main Registry file or SQL Offline store Parquet or warehouse get_historical_features training set, PIT join materialize-incremental scheduled job Redis / Postgres latest value per key feast serve :6566 to model Everything reads definitions from the registry; the feature server refreshes it every registry_ttl_sec seconds.
The pieces built in this guide and the commands that connect them, as documented for Feast 0.67.0 and run locally for this post.

Step 8: Swap in Postgres for the online store and registry

If you already run Postgres and do not want another stateful service, Feast can use it for both the online store and the registry. The file registry rewrites the whole file on every change, which the SQL registry docs call out as a risk with concurrent writers, so move to the SQL registry as soon as more than one job applies or materialises.

# feature_repo/feature_store.yaml  (Postgres variant, feast 0.67.0)
project: payments
provider: local
registry:
  registry_type: sql
  path: postgresql+psycopg://feast:${FEAST_PG_PASSWORD}@localhost:5432/feast
  cache_ttl_seconds: 60
offline_store:
  type: file
online_store:
  type: postgres
  host: localhost
  port: 5432
  database: feast
  db_schema: feast_online
  user: feast
  password: ${FEAST_PG_PASSWORD}
  sslmode: disable          # local only; the default is require
entity_key_serialization_version: 3

After feast apply and a materialise run, Postgres has the registry tables (feature_views, entities, projects and others) in public, and one online table per feature view, named <project>_<feature_view>:

$ psql -U feast feast -c '\dt feast_online.*'
    Schema    |          Name           | Type  | Owner
--------------+-------------------------+-------+-------
 feast_online | payments_customer_stats | table | feast

Per the Postgres store docs, sslmode defaults to require, so a local container without TLS needs sslmode: disable. Set the database time zone to UTC: on my first run my Homebrew cluster had inherited the Mac's local time zone (the Docker command above sets TZ=UTC to avoid this) and the stored event_ts values were shifted by that offset; after ALTER DATABASE feast SET timezone='UTC' and a re-materialise they matched the source.

Step 9: Validate feature definitions in CI

The Feast docs recommend running feast plan on pull requests and feast apply on merge (running Feast in production). I add a second check: build a throwaway registry with SQLite and run a point-in-time assertion on fixture data, so a broken definition or a leaky join fails the PR without touching production. feast -f points the CLI at an alternative feature_store.yaml.

# feature_repo/ci/feature_store.yaml  (paths are relative to feature_repo/)
project: payments
provider: local
registry: data/ci_registry.db
offline_store:
  type: file
online_store:
  type: sqlite
  path: data/ci_online.db
entity_key_serialization_version: 3
# feature_repo/ci/test_features.py
from datetime import datetime, timezone

import pandas as pd
from feast import FeatureStore

store = FeatureStore(fs_yaml_file="ci/feature_store.yaml", repo_path=".")
entity_df = pd.DataFrame({
    "customer_id": [1001],
    "event_timestamp": [datetime(2026, 10, 20, 12, 0, tzinfo=timezone.utc)],
})
df = store.get_historical_features(
    entity_df=entity_df,
    features=store.get_feature_service("fraud_model_v1"),
).to_df()

# The 20 Oct value must win over the 21 Oct value: no future data in training.
assert df.loc[0, "txn_count_7d"] == 18, df.to_string()
assert not df.isna().any().any(), "null features in training set"
print("point-in-time check passed")
# .github/workflows/features.yml
name: feature-definitions
on:
  pull_request:
    paths: ["feature_repo/**"]
jobs:
  validate:
    runs-on: ubuntu-24.04
    defaults:
      run:
        working-directory: feature_repo
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - run: pip install "feast[redis,postgres]==0.67.0"
      - run: feast -f ci/feature_store.yaml plan
      - run: feast -f ci/feature_store.yaml apply
      - run: python ci/test_features.py

Locally this printed point-in-time check passed. To prove the check bites, I copied features.py to dup.py and feast plan failed with ConflictingFeatureViewNames: Multiple FeatureViews with name 'customer_stats' found. The apply against production belongs in a separate workflow on push to main, with registry credentials from your secret store via OIDC rather than a long-lived password. If your pipeline needs a refresher, see building a CI/CD pipeline with GitHub Actions.

Troubleshooting

Symptom Likely cause Fix
ConflictingFeatureViewNames: Multiple FeatureViews with name ... found A copied or helper .py file redefines the view Delete the duplicate or add its path to .feastignore
feast apply runs your data-generation script Feast imports every .py file in the repo Move scripts out or list them in .feastignore
redis.exceptions.ConnectionError: Error 61 connecting to localhost:6379. Connection refused. (111 on Linux) Redis not running, or wrong port in connection_string Start Redis; check redis-cli -p <port> ping from the same host as Feast
get-online-features returns HTTP 422 Input should be a valid dictionary Request sent without Content-Type: application/json Add -H 'Content-Type: application/json'
Online features are all None Never materialised, wrong entity key type, or values older than TTL Run an explicit feast materialize backfill; check join_keys and value_type
First materialize-incremental loaded nothing First run starts at now minus TTL; your data is older Backfill once with feast materialize <start> <end>
Training set has nulls for old labels Label timestamp is more than TTL after the last feature value Raise TTL or fix stale source data; the PIT join only looks back TTL
Postgres connection fails locally with SSL errors sslmode defaults to require Set sslmode: disable for local, verify-full with certs in prod
Postgres event_ts shifted by hours Database time zone not UTC ALTER DATABASE feast SET timezone='UTC', re-materialise
Registry changes lost when two jobs run File registry rewrites the whole file Switch to registry_type: sql

FAQ

Which online store should I use with Feast?

Redis if you need the lowest latency and have several feature views per entity, since Feast reads them in one pipeline. Postgres if you already run it well and want fewer moving parts. Managed options (DynamoDB, Bigtable) make sense when you are deep in one cloud.

Do I need the feature server, or can I call the SDK directly?

Python services can call get_online_features directly. Use feast serve when callers are in other languages, when you want one place to scale, cache the registry and expose metrics, or when you do not want every service holding online store credentials.

How often should I run materialize-incremental?

As often as your freshness requirement and your source data allow. If source tables refresh hourly, materialising every five minutes buys nothing. For sub-minute freshness, push values with a push source or the /push endpoint instead of tightening the schedule.

Is the file registry fine for production?

Only with a single writer. Once CI, materialisation and humans can all write, use the SQL registry on Postgres or MySQL so changes to individual objects are atomic.

When I take over a Feast deployment, the first things I check are boring on purpose: that the SDK version is pinned identically in CI, the materialisation job and the feature server image; that the registry is SQL-backed; that someone gets paged when the newest online event_timestamp is older than the TTL; and that there is a point-in-time test in CI. Everything else is tuning. The official references are the Feast docs, the running in production guide, the feature server reference and the 0.67.0 release notes.

Related: Feature Stores Explained (2026): Online vs Offline, Feast, Tecton, LLMs, How to Run Postgres on Kubernetes with CloudNativePG 1.30 (2026) and How to Deploy an ML Model on Kubernetes with KServe 0.21 (2026).

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related