feast-dev / feast-dev/feast

Feature view ttl is not applied on the standard online retrieval path

Open
#6,821 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
7.3k
Forks
1.4k
Avg merge
3d 16h
Merged PRs (30d)
15

Description

## Expected Behavior

A `FeatureView` with a non-zero `ttl` should not serve a value older than that `ttl` through `get_online_features`, or should at least indicate that the value has expired.

Historical retrieval already honours `ttl`, and the docs state that event timestamps serve the same purpose online — *"Event timestamps are also used to ensure that old feature values aren't served to models during online serving"* ([`docs/getting-started/concepts/feature-retrieval.md`](https://docs.feast.dev/getting-started/concepts/feature-retrieval)) — so the two paths agreeing is the reasonable expectation.

## Current Behavior

`ttl` has no effect on online reads. An expired value is returned, marked `PRESENT`, indistinguishable from a fresh one — and the same value comes back whatever `ttl` is set to.

Reproduced on 0.66.0 with a 1-second `ttl` and a value ~17 minutes old:

```
ttl : 0:00:01
value age : 0.28 hours
to_df value : 0.0023110995534807444
field status : 1 (PRESENT)
```

Response assembly in `sdk/python/feast/utils.py` never compares against the event timestamp it already has to hand:

```python
PRESENT = FieldStatus.PRESENT
NOT_FOUND = FieldStatus.NOT_FOUND
...
feat_values[f_idx][out_idx] = feat_val
feat_statuses[f_idx][out_idx] = PRESENT
```

## Steps to reproduce

```bash
feast init -t local demo && cd demo/feature_repo
# In feature_definitions.py set: ttl=timedelta(seconds=1) on driver_stats_fv
feast apply
# NOT materialize-incremental: with no prior run it derives start_date = end_date - ttl,
# so a 1s ttl materializes nothing.
feast materialize 2021-04-01T00:00:00 "$(date -u +%Y-%m-%dT%H:%M:%S)"
```

```python
from datetime import datetime, timezone
from feast import FeatureStore

STATUS = {0: "INVALID", 1: "PRESENT", 2: "NULL_VALUE", 3: "NOT_FOUND", 4: "OUTSIDE_MAX_AGE"}
store = FeatureStore(repo_path=".")
fv = store.get_feature_view("driver_hourly_stats")
resp = store.get_online_features(
features=["driver_hourly_stats:conv_rate"], entity_rows=[{"driver_id": 1001}]
)

names = list(resp.proto.metadata.feature_names.val)
idx = names.index("conv_rate") # results[0] is the entity column, not a feature
vec = resp.proto.results[idx]
age_s = datetime.now(timezone.utc).timestamp() - vec.event_timestamps[0].seconds

print(f"ttl : {fv.ttl}") # 0:00:01
print(f"value age : {age_s / 3600:.2f} hours") # 0.28
print(f"to_df value : {resp.to_df()['conv_rate'][0]}") # 0.00231...
print(f"field status : {vec.statuses[0]} ({STATUS[vec.statuses[0]]})") # 1 (PRESENT)
```

### Specifications

- **Version:** 0.66.0 (the code path is also present on `master` as of 2026-09)
- **Platform:** macOS, Python 3.13, `feast init -t local` (sqlite online store, file offline store)
- **Subsystem:** online store retrieval (`feast/utils.py`, `OnlineResponse`)

## Possible Solution

The response format already has a slot for this: `FieldStatus.OUTSIDE_MAX_AGE` (`protos/feast/serving/ServingService.proto`).

So the fix is to compare the event timestamp against the feature view's `ttl` where statuses are assigned in `sdk/python/feast/utils.py`, and set that status for expired fields — populating a field the proto already defines, rather than adding anything new.

**Preferably, also withhold the value.** This is what makes `ttl` mean something in practice: `OnlineResponse.to_dict()` reads `feature_vector.values` and never `.statuses`, so `to_df()` callers — the primary Python surface, used throughout the docs — cannot see the status at all and would still receive the expired value. Nulling it where the response is built also reaches non-Python consumers such as the Go feature server and direct HTTP callers. Keeping the status alongside the null loses nothing, since status-aware callers still see why the value is absent.

It does change what existing callers receive, which is worth being deliberate about — but a caller silently relying on values that `ttl` says are expired is the behaviour this issue is about.

If that compatibility concern rules it out, setting the status alone would still be a strict improvement, and is purely additive: callers that ignore statuses see no change, and callers that read them can act on the information for the first time.

---

*Drafted with AI assistance (Claude Code). The reproduction above was executed against feast 0.66.0 — the output is copied from that run, not reconstructed — and all code references were read from the installed source.*

Contributor guide

Open the contributing guide

Research direction

Start in sdk/python/feast/utils.py at OnlineResponse response assembly, then read FieldStatus.OUTSIDE_MAX_AGE in protos/feast/serving/ServingService.proto. Reproduce the expired-value behavior with the provided local feature-repo steps and inspect how to compare event timestamps with the FeatureView ttl. Done means expired online fields report the defined status and the chosen value-handling behavior is covered by the response surfaces described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data-engineering, machine-learning
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.