docs/how-to-guides/feast-operator/02-persistence.md
The offline store, online store, and registry each need a place to store data. The operator supports two persistence patterns for each:
The pattern is the same for all three services; only the store types differ.
| Pattern | Best for | Data survives pod restart? |
|---|---|---|
| File — emptyDir | Dev / CI | No |
| File — PVC (ref) | Single-node prod or testing | Yes (if PVC is retained) |
| File — PVC (create) | Operator-managed storage | Yes |
| DB store | Production, HA, multi-pod | Yes |
The default when no persistence block is set. Data lives on the pod's local disk and is
lost on restart. Suitable for development only.
services:
onlineStore:
server: {} # no persistence block → emptyDir
When you already have a PVC provisioned (e.g. by your storage team):
services:
onlineStore:
persistence:
file:
path: online_store.db # filename relative to mountPath
pvc:
ref:
name: online-pvc # name of the existing PVC
mountPath: /data/online # where the PVC is mounted in the pod
services:
offlineStore:
persistence:
file:
type: duckdb # offline file type: file | dask | duckdb
pvc:
create:
storageClassName: standard
resources:
requests:
storage: 20Gi
mountPath: /data/offline
Omitting storageClassName uses the cluster default StorageClass. Omitting create
entirely creates a PVC with the operator's built-in defaults (1 Gi, default StorageClass).
registry:
local:
persistence:
file:
path: registry.db
pvc:
create: {} # operator defaults: 1 Gi, default StorageClass
mountPath: /data/registry
For production, point the operator at an external database. The operator reads connection
details from a Kubernetes Secret and writes them into feature_store.yaml.
The Secret must contain one key per store component. The key name is the store type
(e.g. postgres, sql, redis). The value is a YAML snippet identical to what you
would write under the corresponding section in feature_store.yaml, minus the type: key
(the operator inserts it from persistence.store.type).
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
# Key for online store type "postgres"
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
db_schema: public
user: feast
password: feast
# Key for registry type "sql" (SQLAlchemy URL)
sql: |
path: postgresql+psycopg://feast:[email protected]:5432/feast #pragma: allowlist secret
cache_ttl_seconds: 60
sqlalchemy_config_kwargs:
echo: false
pool_pre_ping: true
Reference the Secret from the CR:
services:
onlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores # key "postgres" is read automatically
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-data-stores # key "sql" is read automatically
Key lookup rule: the operator looks up the Secret key that matches
persistence.store.type(e.g.type: postgres→ keypostgres). Keep all stores in one Secret or split across multiple — both work.
The Secret key values in the example above hard-code passwords. For production, keep
credentials in a separate Secret and inject them as environment variables using envFrom,
then reference them with ${VAR} substitution in the data-stores Secret value:
apiVersion: v1
kind: Secret
metadata:
name: postgres-creds
stringData:
POSTGRES_USER: feast
POSTGRES_PASSWORD: s3cr3t
---
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
services:
onlineStore:
server:
envFrom:
- secretRef:
name: postgres-creds # expands ${POSTGRES_USER} / ${POSTGRES_PASSWORD}
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores
type | Secret key | SDK docs |
|---|---|---|
sqlite | sqlite | SQLite |
redis | redis | Redis |
postgres | postgres | Postgres |
cassandra | cassandra | Cassandra |
hazelcast | hazelcast | Hazelcast |
hbase | hbase | HBase |
datastore | datastore | Datastore |
dynamodb | dynamodb | DynamoDB |
bigtable | bigtable | Bigtable |
For all store-specific YAML keys (connection options, pool sizes, etc.) see the linked SDK docs — the Secret value accepts the same keys.
type (file) | Notes |
|---|---|
file | Default pandas-based parquet offline store |
dask | Dask-based parallel parquet |
duckdb | DuckDB in-process analytical engine |
For external DB-backed offline stores (BigQuery, Snowflake, Spark, Trino, etc.), use
persistence.store.type and a Secret with the matching key. See
Offline Stores in the SDK docs.
type | Secret key | Notes |
|---|---|---|
file | (file persistence, no Secret) | SQLite-backed file registry |
sql | sql | SQLAlchemy URL — supports PostgreSQL, MySQL, SQLite |
snowflake.registry | snowflake.registry | Snowflake-backed registry |
For sql, the Secret value is a path: (SQLAlchemy URL) plus optional cache_ttl_seconds
and sqlalchemy_config_kwargs:
# feast-data-stores Secret, key "sql"
sql: |
path: postgresql+psycopg://user:pass@host:5432/feast #pragma: allowlist secret
cache_ttl_seconds: 60
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
redis: |
connection_string: redis.feast.svc.cluster.local:6379
---
apiVersion: feast.dev/v1
kind: FeatureStore
metadata:
name: sample-redis
spec:
feastProject: my_project
services:
onlineStore:
server: {}
persistence:
store:
type: redis
secretRef:
name: feast-data-stores
services:
onlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores # reads key "postgres"
offlineStore:
persistence:
file:
type: duckdb
pvc:
create: {}
mountPath: /data/offline
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-data-stores # reads key "sql"
By default the operator looks up the Secret key that matches persistence.store.type (e.g.
type: postgres → key postgres). To use a different key, set secretKeyName:
services:
onlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-data-stores
secretKeyName: my_custom_key # reads key "my_custom_key" instead of "postgres"
This is useful when a single Secret holds configuration for multiple stores of the same type, or when you want a more descriptive key name.
The operator enforces these rules on Secret values at reconciliation time:
type field, its value must match the CR's persistence.store.type.
Otherwise the operator rejects it with an error. Best practice: omit type from the Secret.registry_type field (for registry stores), the same matching rule applies.file or store may be set under each persistence block (enforced by CRD validation).Below are copy-paste-ready Secret YAML snippets for every operator-supported store type. Each snippet shows the Secret data key and the YAML value the operator expects.
Note: omit the
typefield from Secret values — the operator injects it frompersistence.store.type. Including a matchingtypevalue is tolerated but not recommended.
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
redis: |
connection_string: redis.feast.svc.cluster.local:6379
Redis Cluster with SSL:
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
redis: |
redis_type: redis_cluster
connection_string: "redis1:6379,redis2:6379,ssl=true,password=my_password"
CR snippet:
services:
onlineStore:
persistence:
store:
type: redis
secretRef:
name: feast-online-store
SDK reference: Redis
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
db_schema: public
user: feast
password: feast
With SSL:
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
db_schema: public
user: feast
password: feast
sslmode: verify-ca
sslrootcert_path: /path/to/server-ca.pem
CR snippet:
services:
onlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-online-store
SDK reference: Postgres
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
cassandra: |
hosts:
- 192.168.1.1
- 192.168.1.2
- 192.168.1.3
keyspace: KeyspaceName
port: 9042
username: user
password: secret
protocol_version: 5
load_balancing:
local_dc: datacenter1
load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy)
read_concurrency: 100
write_concurrency: 100
CR snippet:
services:
onlineStore:
persistence:
store:
type: cassandra
secretRef:
name: feast-online-store
SDK reference: Cassandra
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
snowflake.online: |
account: snowflake_deployment.us-east-1
user: user_login
password: user_password
role: SYSADMIN
warehouse: COMPUTE_WH
database: FEAST
CR snippet:
services:
onlineStore:
persistence:
store:
type: snowflake.online
secretRef:
name: feast-online-store
SDK reference: Snowflake
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
dynamodb: |
region: us-west-2
batch_size: 100
CR snippet:
services:
onlineStore:
persistence:
store:
type: dynamodb
secretRef:
name: feast-online-store
SDK reference: DynamoDB
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
bigtable: |
project_id: my_gcp_project
instance: my_bigtable_instance
CR snippet:
services:
onlineStore:
persistence:
store:
type: bigtable
secretRef:
name: feast-online-store
SDK reference: Bigtable
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
datastore: |
project_id: my_gcp_project
namespace: my_datastore_namespace
CR snippet:
services:
onlineStore:
persistence:
store:
type: datastore
secretRef:
name: feast-online-store
SDK reference: Datastore
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
mysql: |
host: mysql.feast.svc.cluster.local
port: 3306
database: feast
user: feast
password: feast
CR snippet:
services:
onlineStore:
persistence:
store:
type: mysql
secretRef:
name: feast-online-store
SDK reference: MySQL
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
hazelcast: |
cluster_name: dev
cluster_members:
- "localhost:5701"
key_ttl_seconds: 36000
CR snippet:
services:
onlineStore:
persistence:
store:
type: hazelcast
secretRef:
name: feast-online-store
SDK reference: Hazelcast
apiVersion: v1
kind: Secret
metadata:
name: feast-online-store
stringData:
hbase: |
host: hbase-thrift.feast.svc.cluster.local
port: "9090"
connection_pool_size: 4
CR snippet:
services:
onlineStore:
persistence:
store:
type: hbase
secretRef:
name: feast-online-store
SDK reference: HBase
The following types also use the same pattern (persistence.store.type + secretRef).
Place the driver-specific YAML keys from the SDK docs under the matching Secret key:
type | Secret key | SDK docs |
|---|---|---|
sqlite | sqlite | SQLite |
singlestore | singlestore | SingleStore |
elasticsearch | elasticsearch | Elasticsearch |
qdrant | qdrant | Qdrant |
couchbase.online | couchbase.online | Couchbase |
milvus | milvus | Milvus |
mongodb | mongodb | MongoDB |
hybrid | hybrid | Hybrid |
Offline DB stores follow the same pattern. The type field tells the operator which
store driver to use; the Secret value holds the connection parameters.
apiVersion: v1
kind: Secret
metadata:
name: feast-offline-store
stringData:
snowflake.offline: |
account: snowflake_deployment.us-east-1
user: user_login
password: user_password
role: SYSADMIN
warehouse: COMPUTE_WH
database: FEAST
schema: PUBLIC
CR snippet:
services:
offlineStore:
persistence:
store:
type: snowflake.offline
secretRef:
name: feast-offline-store
SDK reference: Snowflake
apiVersion: v1
kind: Secret
metadata:
name: feast-offline-store
stringData:
bigquery: |
dataset: feast_bq_dataset
project_id: my_gcp_project
CR snippet:
services:
offlineStore:
persistence:
store:
type: bigquery
secretRef:
name: feast-offline-store
SDK reference: BigQuery
apiVersion: v1
kind: Secret
metadata:
name: feast-offline-store
stringData:
postgres: |
host: postgres.feast.svc.cluster.local
port: 5432
database: feast
db_schema: public
user: feast
password: feast
CR snippet:
services:
offlineStore:
persistence:
store:
type: postgres
secretRef:
name: feast-offline-store
SDK reference: Postgres
type | Secret key | SDK docs |
|---|---|---|
redshift | redshift | Redshift |
spark | spark | Spark |
trino | trino | Trino |
athena | athena | Athena |
mssql | mssql | MSSQL |
couchbase.offline | couchbase.offline | Couchbase |
clickhouse | clickhouse | ClickHouse |
ray | ray | Ray |
oracle | oracle | Oracle |
The most common production registry. Uses a SQLAlchemy URL to connect to PostgreSQL, MySQL, or SQLite:
apiVersion: v1
kind: Secret
metadata:
name: feast-registry-store
stringData:
sql: |
path: postgresql+psycopg://feast:[email protected]:5432/feast #pragma: allowlist secret
cache_ttl_seconds: 60
sqlalchemy_config_kwargs:
echo: false
pool_pre_ping: true
CR snippet:
services:
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-registry-store
SDK reference: SQL Registry
apiVersion: v1
kind: Secret
metadata:
name: feast-registry-store
stringData:
snowflake.registry: |
account: snowflake_deployment.us-east-1
user: user_login
password: user_password
role: SYSADMIN
warehouse: COMPUTE_WH
database: FEAST
schema: PUBLIC
cache_ttl_seconds: 60
CR snippet:
services:
registry:
local:
persistence:
store:
type: snowflake.registry
secretRef:
name: feast-registry-store
SDK reference: Snowflake Registry
You can combine all store configurations into a single Secret:
apiVersion: v1
kind: Secret
metadata:
name: feast-data-stores
stringData:
redis: |
connection_string: redis.feast.svc.cluster.local:6379
snowflake.offline: |
account: snowflake_deployment.us-east-1
user: user_login
password: user_password
role: SYSADMIN
warehouse: COMPUTE_WH
database: FEAST
schema: PUBLIC
sql: |
path: postgresql+psycopg://feast:[email protected]:5432/feast #pragma: allowlist secret
cache_ttl_seconds: 60
---
apiVersion: feast.dev/v1
kind: FeatureStore
metadata:
name: production-store
spec:
feastProject: my_project
services:
onlineStore:
persistence:
store:
type: redis
secretRef:
name: feast-data-stores
offlineStore:
persistence:
store:
type: snowflake.offline
secretRef:
name: feast-data-stores
registry:
local:
persistence:
store:
type: sql
secretRef:
name: feast-data-stores
The batchEngine is the only operator component that uses a ConfigMap rather than a
Secret for its configuration. The ConfigMap must contain a YAML value under key config
(default) or the key specified in configMapKey.
Unlike store Secrets, the batch engine ConfigMap value must include the type field:
apiVersion: v1
kind: ConfigMap
metadata:
name: feast-batch-engine
data:
config: |
type: spark
master: local
spark_conf:
spark.executor.memory: 4g
---
apiVersion: feast.dev/v1
kind: FeatureStore
metadata:
name: sample
spec:
feastProject: my_project
batchEngine:
configMapRef:
name: feast-batch-engine
# configMapKey: config # optional, defaults to "config"
See Guide 6 — Batch & Jobs for full details.
| Symptom | Likely cause | Fix |
|---|---|---|
secret key X doesn't exist in secret Y | The Secret key name doesn't match the store type | Either rename the Secret key to match type, or set secretKeyName in the CR |
secret X contains invalid value | The Secret value is not valid YAML | Check indentation and quoting in the stringData value |
contains tag named type with value X | The Secret includes a type field that doesn't match the CR's persistence.store.type | Remove type from the Secret value, or correct it to match |
invalid secret X for offline store | The referenced Secret doesn't exist | Create the Secret in the same namespace as the FeatureStore CR |
One selection required between file or store | Both file and store are set under a persistence block | Keep only one — choose either file persistence or store (DB) persistence |