Skip to content

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

Description

@pedgar

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) — 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:

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

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)"
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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions