You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/how-to-guides/online-server-performance-tuning.md
+121-5Lines changed: 121 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,6 +31,10 @@ When the server processes a `get_online_features()` call, it groups the requeste
31
31
32
32
**Guideline:** For features that share the same entity key and are frequently requested together, consolidate them into a **single feature view**. This reduces the number of store round-trips per request. Split feature views only when features have different entities, different materialization schedules, or different data source update frequencies.
33
33
34
+
{% hint style="info" %}
35
+
**Redis exception:** The Redis online store overrides `get_online_features()` to batch all `HMGET` commands across every feature view into a **single pipeline execution**. Because all feature views for the same entity share one Redis hash key, the number of Redis round trips is always **1**, regardless of how many feature views the request touches. This means the "fewer feature views" guideline is less critical for Redis than for other stores — but consolidating feature views still reduces serialization and protobuf overhead at the application layer.
36
+
{% endhint %}
37
+
34
38
### Feature services are free
35
39
36
40
A [Feature Service](../getting-started/concepts/feature-retrieval.md) is a named collection of feature references — it's a convenience grouping, not a separate storage or execution unit. Using a feature service adds only a registry lookup (cached) compared to listing features individually. There is no performance penalty for using feature services, and they are the recommended way to define stable, versioned feature sets for production models.
@@ -229,7 +233,7 @@ The online store is the single largest factor in `get_online_features()` latency
229
233
230
234
| Store | Typical p50 latency | Async read | Best for | Key trade-off |
| **Redis / Dragonfly** | < 1 ms | No (threadpool) | Ultra-low latency, high throughput | Requires in-memory capacity for your dataset |
236
+
| **Redis / Dragonfly** | < 1 ms | No (threadpool) | Ultra-low latency, high throughput; all FV reads batched into 1 pipeline | Requires in-memory capacity for your dataset |
233
237
| **DynamoDB** | 2–5 ms | Yes | Serverless, auto-scaling on AWS | Pay-per-request cost; batch API limits (100 items) |
234
238
| **PostgreSQL** | 3–10 ms | No (threadpool) | Teams with existing Postgres infra | Connection pooling needed at scale |
235
239
| **MongoDB** | 2–5 ms | Yes | Flexible schema, async-native | Requires index tuning for large datasets |
@@ -259,7 +263,7 @@ The feature server can read from the online store using either an **async** or *
| **PostgreSQL** | Implemented | No | Has `online_read_async` but does not yet advertise via `async_supported`; uses sync/threadpool path |
262
-
| **Redis** | Implemented | No | Has `online_read_async` but does not yet advertise via `async_supported`; uses sync/threadpool path |
266
+
| **Redis** | Implemented | **Yes** | `online_read_async` and `online_write_batch_async` both implemented; uses sync/threadpool path for `get_online_features` (overridden with batched single pipeline) |
263
267
| All others | No | No | Fall back to sync with `run_in_threadpool()` |
skip_dedup: false # set true for initial bulk loads to halve write round trips
332
337
```
333
338
334
339
- Use `redis_cluster` for horizontal partitioning across shards.
335
-
- Set `key_ttl_seconds` to auto-expire stale feature data, keeping memory usage bounded.
340
+
- Set `key_ttl_seconds` to auto-expire stale feature data, keeping memory usage bounded. This is a **key-level** TTL — it expires the entire entity hash (all feature views for that entity) together.
336
341
- Ensure the Redis instance is in the **same availability zone** as the feature server pods to minimize network round-trips.
337
342
343
+
#### Batched multi-feature-view reads
344
+
345
+
The Redis online store overrides `get_online_features()` to issue all `HMGET` commands — across every feature view in the request — in a **single pipeline execution**. This reduces Redis round trips from `N` (one per feature view) to `1` regardless of request size.
This means the latency cost of adding feature views to a Redis-backed request is primarily **serialization and protobuf overhead** at the application layer, not Redis network latency.
355
+
356
+
#### Per-feature-view TTL at retrieval
357
+
358
+
Set a `ttl` on individual `FeatureView` objects to enforce staleness checks at read time. Features whose stored event timestamp is older than the TTL are returned as `NOT_FOUND` rather than stale values:
359
+
360
+
```python
361
+
from datetime import timedelta
362
+
363
+
driver_stats = FeatureView(
364
+
name="driver_stats",
365
+
entities=["driver"],
366
+
ttl=timedelta(hours=6), # features older than 6 hours returned as NOT_FOUND
367
+
schema=[...],
368
+
source=driver_stats_source,
369
+
)
370
+
```
371
+
372
+
This is the correct way to enforce per-feature-view TTL semantics. The `key_ttl_seconds` config option applies a Redis `EXPIRE` to the whole entity hash (all feature views share one expiry), so it cannot enforce different TTLs per feature view.
373
+
374
+
#### Write throughput: `skip_dedup`
375
+
376
+
For initial bulk loads or append-only materialization pipelines:
377
+
378
+
```yaml
379
+
online_store:
380
+
type: redis
381
+
connection_string: "localhost:6379"
382
+
skip_dedup: true
383
+
```
384
+
385
+
With `skip_dedup: true`, `online_write_batch()` skips the existing-timestamp read pipeline and writes all values directly in a single pass, halving write round trips. Use only when you can guarantee write ordering — under concurrent writers, an older record can overwrite a newer one.
386
+
338
387
### Cassandra / ScyllaDB tuning
339
388
340
389
Cassandra and ScyllaDB share the same Feast connector. The key read-path knobs are concurrency and data-center-aware routing:
@@ -421,7 +470,7 @@ Different online stores have different optimal batch sizes for `get_online_featu
421
470
| Store | Default batch size | Max batch size | Recommendation |
| DynamoDB | 100 | 100 (API limit) | Keep at 100; tune `max_read_workers` for parallelism |
424
-
| Redis | N/A (pipelined) | N/A | Redis pipelines all keys in one round-trip; no batch tuning needed |
473
+
| Redis | N/A (pipelined) | N/A | All entity keys **and** all feature views are batched into a single pipeline; no batch tuning needed |
425
474
| PostgreSQL | N/A (single query) | N/A | Single `SELECT ... WHERE key IN (...)` query; tune connection pool instead |
426
475
427
476
Profile your workload by measuring `feast_feature_server_online_store_read_duration_seconds` (see [Metrics setup](#metrics-setup-prometheus--opentelemetry)) across different entity counts to find the sweet spot.
@@ -980,12 +1029,79 @@ registry:
980
1029
981
1030
---
982
1031
1032
+
## Materialization write performance
1033
+
1034
+
Materialization (`feast materialize` / `feast materialize-incremental`) reads features from the offline store and writes them to the online store. For large feature views, two common bottlenecks arise: **memory exhaustion** during proto conversion and **write throughput** to the online store.
1035
+
1036
+
### Memory: `online_write_batch_size`
1037
+
1038
+
By default, Feast converts the entire Arrow table returned by the offline store into Python protobuf objects in a single pass before writing to the online store. For datasets with millions of rows this can consume tens of gigabytes of memory on the materialization worker.
1039
+
1040
+
Set `online_write_batch_size` in `feature_store.yaml` to break the write into manageable chunks:
1041
+
1042
+
```yaml
1043
+
materialization:
1044
+
online_write_batch_size: 10000 # rows per write batch
1045
+
```
1046
+
1047
+
Each chunk is independently converted and written, keeping peak memory proportional to the batch size rather than the full dataset. This is supported by the **local, Spark, and Ray** compute engines.
1048
+
1049
+
| Dataset size | Without batching | With `online_write_batch_size: 10000` |
- **Larger batches** (50 000+): fewer write calls to the online store, lower overhead per row — good when worker memory allows.
1058
+
- **Smaller batches** (1 000–5 000): lower peak memory — necessary for memory-constrained workers or very wide feature views (many features per row).
1059
+
- For **Redis**: pipeline overhead per batch is negligible; a batch size of 10 000–50 000 is a good starting point.
1060
+
- For **DynamoDB**: each batch maps to one or more `BatchWriteItem` calls (max 25 items per call); a larger `online_write_batch_size` amortizes the per-call overhead but doesn't change the 25-item DynamoDB limit.
1061
+
1062
+
See the [feature-store-yaml reference](../reference/feature-repository/feature-store-yaml.md#online_write_batch_size) for the complete option documentation.
Each feature view in a `feast materialize` call is materialized sequentially by the local engine. To materialize multiple feature views in parallel, use a job orchestrator (Airflow, Kubernetes Jobs) and materialize one feature view per job:
Alternatively, use the **Spark** or **Ray** compute engines which distribute the work across a cluster.
1075
+
1076
+
### Redis: combine with `skip_dedup` for bulk reloads
1077
+
1078
+
When performing a full historical reload into Redis (not an incremental update), combine `online_write_batch_size` with `skip_dedup` for maximum throughput:
1079
+
1080
+
```yaml
1081
+
materialization:
1082
+
online_write_batch_size: 50000 # large chunks — memory is bounded
`skip_dedup: true` eliminates the timestamp-read pipeline before each write (see [Redis tuning](#redis-tuning)), while `online_write_batch_size` prevents the write worker from converting the entire dataset into memory at once.
1091
+
1092
+
{% hint style="warning" %}
1093
+
Reset `skip_dedup` to `false` (or remove it) after the bulk reload. Under normal incremental materialization, deduplication prevents older feature values from overwriting newer ones.
1094
+
{% endhint %}
1095
+
1096
+
---
1097
+
983
1098
## Further reading
984
1099
985
1100
- [Scaling Feast](./scaling-feast.md) — Horizontal scaling, HPA, KEDA, and HA in detail
986
1101
- [Python Feature Server](../reference/feature-servers/python-feature-server.md) — CLI flags, metrics reference, and API endpoints
987
1102
- [OpenTelemetry Integration](../getting-started/components/open-telemetry.md) — Full OTEL setup with Prometheus Operator
988
1103
- [DynamoDB Online Store](../reference/online-stores/dynamodb.md) — Store-specific configuration and performance tuning
989
1104
- [PostgreSQL Online Store](../reference/online-stores/postgres.md) — Connection pooling and SSL configuration
- [feature_store.yaml reference](../reference/feature-repository/feature-store-yaml.md) — Full configuration reference including `materialization` options
Copy file name to clipboardExpand all lines: docs/how-to-guides/running-feast-in-production.md
+9Lines changed: 9 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -75,6 +75,15 @@ Feast keeps the history of materialization in its registry so that the choice co
75
75
76
76
However, the amount of work can quickly outgrow the resources of a single machine. That happens because the materialization job needs to repackage all rows before writing them to an online store. That leads to high utilization of CPU and memory. In this case, you might want to use a job orchestrator to run multiple jobs in parallel using several workers. Kubernetes Jobs or Airflow are good choices for more comprehensive job orchestration.
77
77
78
+
For large datasets, you can also reduce peak memory on the materialization worker by setting `online_write_batch_size` in `feature_store.yaml`. This breaks the proto conversion and write into chunks instead of loading the entire dataset into memory at once:
79
+
80
+
```yaml
81
+
materialization:
82
+
online_write_batch_size: 10000# rows per write batch; reduces peak memory proportionally
83
+
```
84
+
85
+
See the [Materialization write performance](./online-server-performance-tuning.md#materialization-write-performance) guide for sizing recommendations and the full option reference in [feature_store.yaml](../reference/feature-repository/feature-store-yaml.md#online_write_batch_size).
86
+
78
87
If you are using Airflow as a scheduler, Feast can be invoked through a [PythonOperator](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/python.html) after the [Python SDK](https://pypi.org/project/feast/) has been installed into a virtual environment and your feature repo has been synced:
Copy file name to clipboardExpand all lines: docs/reference/feature-repository/feature-store-yaml.md
+64Lines changed: 64 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -25,5 +25,69 @@ The following top-level configuration options exist in the `feature_store.yaml`
25
25
* **offline_store** — Configures the offline store.
26
26
* **project** — Defines a namespace for the entire feature store. Can be used to isolate multiple deployments in a single installation of Feast. Should only contain letters, numbers, and underscores.
27
27
* **engine** - Configures the batch materialization engine.
Please see the [RepoConfig](https://rtd.feast.dev/en/latest/#feast.repo_config.RepoConfig) API reference for the full list of configuration options.
31
+
32
+
---
33
+
34
+
## `materialization` configuration
35
+
36
+
The `materialization` block controls how Feast reads from the offline store and writes to the online store during `feast materialize` / `feast materialize-incremental` runs.
37
+
38
+
{% code title="feature_store.yaml" %}
39
+
```yaml
40
+
project: my_feature_repo
41
+
registry: data/registry.db
42
+
provider: local
43
+
online_store:
44
+
type: redis
45
+
connection_string: "localhost:6379"
46
+
materialization:
47
+
online_write_batch_size: 10000 # write rows in chunks of 10 000
48
+
pull_latest_features: false # pull full time range (default)
Controls how many rows are converted to protobuf and written to the online store per batch during materialization.
59
+
60
+
**Default behaviour (`null`):** All rows fetched from the offline store are converted to protobuf in a single in-memory operation before writing. This is fast but can exhaust memory for large datasets — every row must be held as a Python proto object simultaneously.
61
+
62
+
**With `online_write_batch_size` set:** The Arrow table returned by the offline store is split into chunks of at most `online_write_batch_size` rows. Each chunk is converted and written independently, keeping peak memory proportional to the batch size rather than the full dataset size.
63
+
64
+
```yaml
65
+
# Recommended for datasets > a few million rows or memory-constrained workers
| < 1 M rows | Any | `null` (default — single batch is fine) |
75
+
| 1–10 M rows | ≥ 4 GB | `50000` |
76
+
| 10–100 M rows | ≥ 8 GB | `10000` |
77
+
| > 100 M rows | Any | `5000`–`10000` |
78
+
79
+
A smaller batch size reduces peak memory at the cost of more `online_write_batch` calls to the online store. For Redis, each call is a pipelined batch, so the overhead is low. For stores with higher per-call latency (e.g. DynamoDB), prefer larger batch sizes.
80
+
81
+
{% hint style="info" %}
82
+
`online_write_batch_size`is applied **per feature view** within a single materialization job. If you materialize five feature views in parallel, peak memory is `5 × batch_size × bytes_per_row`.
83
+
{% endhint %}
84
+
85
+
### `pull_latest_features`
86
+
87
+
| Field | Type | Default |
88
+
| --- | --- | --- |
89
+
| `pull_latest_features` | `bool` | `false` |
90
+
91
+
When `false` (default), the offline store retrieves **all** feature values within the requested time range for each entity.
92
+
93
+
When `true`, only the **latest** value per entity is retrieved. This reduces I/O and memory for feature views where historical values are not needed (e.g., slowly changing dimensions). It is equivalent to running a `GROUP BY entity, MAX(event_timestamp)` on the offline data before writing.
0 commit comments