DataLane
(updated )4 min readRedis

Redis Features: Cache the Online Path, Never the Source of Truth

We trained on warehouse snapshots and served from Redis. TTL eviction and a stampede later, the model looked broken. Online features are a cache, not a fact table.

By Dinesh Chandra

Illustrated overview of Redis Features: Cache the Online Path, Never the Source of Truth
Table of contents

The churn model’s live precision fell off a cliff on a Thursday. Batch backtests were fine. Serving logs showed orders_30d = 0 for users we knew were active. Redis had evicted the hash under memory pressure. The API treated a miss as zero, not as “go compute.” Product thought the model had died. The cache had.

Worse: training still joined a warehouse snapshot as-of the event. Serving read whatever Redis had at request time, if it had anything. We had two definitions of the same feature name and a TTL of six hours that nobody had matched to the training window. That is leakage and silence at the same time.

I treat Redis as an online materialization of a warehouse feature, with a miss path and a stampede plan. I do not treat it as a fact table. The MLOps post is the join discipline. This is the cache that sits in front of it.

Misses are normal. Zeros are a lie

maxmemory plus an eviction policy means keys go away. TTL means keys go away on a timer. Both are working as designed. The serving path has to know the difference between “feature is zero” and “feature is absent.”

import json
import time
from redis import Redis

redis = Redis.from_url("redis://features:6379/0")
FEATURE_TTL_S = 6 * 3600

def get_orders_30d(user_id: str) -> int:
    key = f"feat:orders_30d:{user_id}"
    raw = redis.get(key)
    if raw is not None:
        return int(raw)

    # Miss: compute from the same SQL the training snapshot uses.
    value = warehouse_orders_30d(user_id)
    # SET NX + TTL, or a lock, so expiry does not stampede.
    redis.set(key, value, ex=FEATURE_TTL_S, nx=True)
    if redis.get(key) is None:
        redis.set(key, value, ex=FEATURE_TTL_S)
    return value

The warehouse function is the source of truth. Redis is the hot copy. If you cannot run that SQL for a miss, you should not be serving the model from Redis.

flowchart TD
  req["Serving request"] --> cache{"Redis hit?"}
  cache -->|yes| model["Score"]
  cache -->|no| lock["Single-flight fill"]
  lock --> wh["Warehouse snapshot SQL"]
  wh --> set["SET with TTL"]
  set --> model
  evict["TTL or eviction"] --> miss["Next request misses"]
  miss --> lock

A miss recomputes. A miss that becomes zero is how a good model looks broken.

Training snapshot must match the fill

If training uses orders_30d as-of event_at from a type-2 table, and serving uses “whatever is in Redis right now,” you have two features. The honest online path is: fill Redis from the current row of that same model, on a schedule, and accept that online is “as of last fill,” not “as of an arbitrary historical event.”

Historical training rows never come from Redis. They come from the warehouse history. Mixing them is how notebooks beat production.

I keep a daily sample: pull N keys from Redis, recompute from batch SQL, diff. The parity habit in the MLOps post is the same check. Redis just makes the mismatch look like infrastructure.

Cache stampede

When a popular key expires, every replica of the API will miss at once and hit the warehouse. That is a self-inflicted load spike. Single-flight with a short lock, jitter the TTL, or refresh before expiry on a worker. Do not “retry harder” on the miss path.

I also do not persist Redis as the audit of what we served. If you need that, log the feature vector next to the score, or you will never reproduce a Thursday.

Pitfalls

GET miss mapped to 0. You taught the model that unknown users look like inactive ones.

No TTL and no maxmemory policy. You will OOM the instance and lose everything at once instead of gradually.

Training from Redis dumps. You cannot point-in-time join a cache.

Filling Redis from a different SQL than dbt. Train/serve skew with extra steps.

Using Redis lists as an event log. That is a broker’s job. Redis will forget the prefix you needed.

What this means for your pipelines

Online features belong in Redis when the latency SLO requires it. Facts belong in the warehouse, with history you can as-of join. I fill the cache from the same definition I train on, I treat eviction as a miss, and I single-flight the fill.

The Thursday drop was not a model regression. It was a cache that we had promoted to a database, then shrugged when memory ran out. Put the record in silver. Let Redis be fast and forgetful on purpose.

Share this post:X / TwitterLinkedIn

Enjoyed this post?

Get the next one in your inbox — one email a week, no spam.

Newsletter signup is not live yet. Use the contact form if you want to be notified.

↑↓ navigate openesc close