Feature view ttl is not applied on the standard online retrieval path
- 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
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