Russell Brooks
05/07/2026, 2:23 PMMarvin
05/07/2026, 2:23 PMMarvin
05/07/2026, 2:23 PMRussell Brooks
05/07/2026, 2:25 PMMarvin
05/07/2026, 2:25 PMMarvin
05/07/2026, 2:28 PMprefect-helm Redis subchart for flow result storage and task caching, with one important distinction:
The Helm Redis subchart is primarily there for Prefect server/background services messaging, but your flows can also use that same Redis instance as a storage backend if they can reach it over the network.
Short version
- prefect-helm Redis subchart => gives you a Redis service in the cluster
- Prefect server may use it for messaging/broker/cache
- Your flow code can also use it for:
- result storage
- task caching (by persisting cached results to Redis)
A good mental model is:
- server Redis usage = internal Prefect platform coordination
- flow Redis usage = where your task/flow results live
## 1. When this works well
This is most useful when:
- your workers/run infrastructure are in the same Kubernetes cluster
- your flow runs can resolve the Redis service name
- you want a simple shared cache/result backend
If your workers run outside the cluster, they generally won’t be able to use the in-cluster Redis service unless you expose it.
## 2. Enable the Redis subchart in prefect-helm
At a minimum, in your Helm values:
yaml
redis:
enabled: true
architecture: standalone
auth:
password: "my-redis-password"
A few notes:
- Prefect currently expects standalone Redis here, not clustered Redis.
- The Redis subchart is convenient, but for production many teams use an external managed Redis instead.
If you’re also using Redis for Prefect server background services, you may also have config like:
yaml
backgroundServices:
runAsSeparateDeployment: true
messaging:
broker: prefect_redis.messaging
cache: prefect_redis.messaging
## 3. What hostname do flows use?
With the Bitnami Redis chart used as a subchart, the Redis service is typically something like:
text
<helm-release-name>-redis-master
So if your Helm release is named prefect, the host is often:
text
prefect-redis-master
If your flow run is in the same namespace, that short name often works. Otherwise use the full DNS name, e.g.:
text
prefect-redis-master.<namespace>.svc.cluster.local
## 4. Result storage in Prefect 3.x
In Prefect 3.x, one straightforward option is the built-in RedisStorageContainer block.
Its constructor supports:
- host
- port
- db
- username
- password
- connection_string
So you can create a block pointing at the Redis subchart service and then use that block as result_storage.
## 5. Example: create a Redis block for result storage
python
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
redis_storage = RedisStorageContainer(
host="prefect-redis-master",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
)
redis_storage.save("helm-redis-results", overwrite=True)
I’d recommend using a different DB number for flow results than whatever the server messaging config uses.
For example:
- db=0 for Prefect server/internal use
- db=1 for flow results
- db=2 for another purpose if needed
## 6. Example: use Redis as result storage in a flow
python
from prefect import flow, task
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
redis_storage = RedisStorageContainer(
host="prefect-redis-master",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
)
@task(persist_result=True)
def compute(x: int) -> int:
print(f"running compute({x})")
return x * 10
@flow(persist_result=True, result_storage=redis_storage)
def my_flow(x: int) -> int:
return compute(x)
if __name__ == "__main__":
print(my_flow(5))
That makes Redis the place where persisted results are written.
## 7. Example: use Redis-backed persistence for task caching
In Prefect, caching works by:
- computing a cache key
- checking for an existing persisted result for that keyMarvin
05/07/2026, 2:28 PMpersist_result=True
- a cache policy like INPUTS
- Redis as the task/flow result storage
Example:
python
from prefect import flow, task
from prefect.cache_policies import INPUTS
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
redis_storage = RedisStorageContainer(
host="prefect-redis-master",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
)
@task(
persist_result=True,
cache_policy=INPUTS,
result_storage=redis_storage,
)
def expensive_calculation(x: int) -> int:
print(f"actually computing for {x}")
return x * 100
@flow
def cached_flow():
a = expensive_calculation(7)
b = expensive_calculation(7)
c = expensive_calculation(8)
return a, b, c
if __name__ == "__main__":
print(cached_flow())
What you should see:
- first call with 7 runs normally
- second call with 7 should come from cache
- call with 8 runs normally
## 8. Example: save the block once, then load it by name
Often the nicest pattern is:
1. create/save the block once
2. reference it later
Setup script:
python
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
RedisStorageContainer(
host="prefect-redis-master",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
).save("helm-redis-results", overwrite=True)
Then in your flow code:
python
from prefect import flow, task
from prefect.cache_policies import INPUTS
from prefect.blocks.redis import RedisStorageContainer
redis_storage = RedisStorageContainer.load("helm-redis-results")
@task(
persist_result=True,
cache_policy=INPUTS,
result_storage=redis_storage,
)
def transform(x: int) -> int:
print(f"transforming {x}")
return x + 1
@flow(persist_result=True, result_storage=redis_storage)
def my_flow():
return transform(3)
## 9. Can I use prefect-redis too?
Yes — there is also the prefect-redis collection, which includes RedisDatabase.
That can also be used as a Redis-backed block, but if your goal is result storage, I’d usually start with the built-in prefect.blocks.redis.RedisStorageContainer since it maps directly to storage usage.
The prefect-redis package is also used for things like Prefect messaging integration and includes other Redis utilities.
## 10. Practical recommendation
If you want to use the Helm Redis for both server internals and flow caching/results, I’d recommend:
- same Redis instance
- different logical DBs
- don’t mix credentials in code if possible
- store credentials in a Prefect block / secret / env vars
Example split:
- DB `0`: server/background services
- DB `1`: flow result storage + task cache
## 11. Common gotchas
### A) Workers must be able to reach Redis
If your flow runs happen outside the Kubernetes cluster, prefect-redis-master will not resolve unless exposed.
### B) Caching is not “just Redis”
Redis is the storage backend here. Prefect caching still depends on:
- persist_result=True
- a cache policy or cache key function
### C) Ephemeral Redis means ephemeral cache/results
If the Redis deployment is not persistent and gets recreated, your cached results are gone.
### D) Don’t assume server Redis config automatically applies to flows
The Helm chart wiring for server messaging does not automatically make flow result storage use Redis. You still need to configure your flow or deployment code to use a Redis block.
## 12. Minimal end-to-end example
Here’s a compact example using the in-cluster Redis service from the Helm chart:
```python
from prefect import flow, task
from prefect.cache_policies import INPUTS
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
redis_storage = RedisStorageContainer(
host="prefect-redis-master",Marvin
05/07/2026, 2:28 PMRussell Brooks
05/07/2026, 2:40 PMMarvin
05/07/2026, 2:40 PMMarvin
05/07/2026, 2:43 PMRedisStorageContainer = the built-in Prefect block I’d use for result storage
- RedisDatabase = the prefect-redis integration block, better when you want direct Redis client access in your flow/task code
- If you only see RedisDatabase in the UI, it usually means that block type has been registered, while RedisStorageContainer has not
## What’s the actual difference?
### RedisStorageContainer
Import path:
python
from prefect.blocks.redis import RedisStorageContainer
Best for:
- result storage
- reading/writing bytes by key/path
- “treat Redis like a storage backend”
Think of it as:
- a storage block
- meant for result_storage=...
Example use:
python
@flow(result_storage=redis_storage, persist_result=True)
def my_flow():
...
### RedisDatabase
Import path:
python
from prefect_redis.blocks import RedisDatabase
Best for:
- direct Redis usage in code
- getting a Redis client
- custom set/get, pubsub, commands, etc.
Think of it as:
- a Redis connection/credentials block
- more natural when you want to do Redis operations yourself
## Why do I only see RedisDatabase in the UI?
Because block types only show up in the UI if they’ve been registered with the Prefect server.
You likely have:
- prefect-redis installed
- and RedisDatabase registered
But RedisStorageContainer is not automatically showing up just because it exists in core Prefect.
So the UI behavior you’re seeing is normal.
## What I recommend for your use case
Since you asked about using the Helm Redis subchart:
- for Prefect result storage / cache persistence: use RedisStorageContainer
- for manual Redis reads/writes in tasks: use RedisDatabase
If your main goal is:
“use Redis as Prefect result storage and cache backend”then
RedisStorageContainer is the clearer fit.
## Important nuance: you do not need the UI block type to use it in code
Even if RedisStorageContainer is not visible in the UI, you can still use it in Python if the package/import is available in the environment where your code runs.
Example:
python
from prefect import flow, task
from prefect.cache_policies import INPUTS
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
redis_storage = RedisStorageContainer(
host="prefect-redis-master",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
)
@task(
persist_result=True,
cache_policy=INPUTS,
result_storage=redis_storage,
)
def compute(x: int) -> int:
print(f"computing {x}")
return x * 2
@flow(persist_result=True, result_storage=redis_storage)
def my_flow():
return compute(5)
That works even if you never create the block in the UI.
## If you want RedisStorageContainer to appear in the UI
You need to register that block type with your Prefect server.
A simple way is to make a file like this:
python
from prefect.blocks.redis import RedisStorageContainer
Then run:
bash
prefect block register -f register_redis.py
After that, the block type should become available in the UI.
## Another important caveat: server env vs worker env
There are two places that matter:
### 1. Prefect server/web UI environment
This is what affects:
- whether the block type is registered and visible in the UI
### 2. Worker / flow runtime environment
This is what affects:
- whether flows can actually import and use the block at runtime
So even if you register it and see it in the UI, your flow still needs the right Python dependencies where it runs.
For RedisStorageContainer, that means the runtime environment needs the Redis Python dependency available.
## Practical advice
If you want the least confusing path:
### Option A: just use RedisStorageContainer in code
Good when:
- you don’t care whether it shows in the UIMarvin
05/07/2026, 2:43 PMRedisStorageContainer and use it as a named block
Good when:
- you want to manage it in the UI
- you want deployments to reference a saved block
## If you want to stick with what you already see in the UI
Since you already see RedisDatabase, you can use that too, especially if it already works in your environment.
But conceptually:
- RedisDatabase = “I have a Redis connection”
- RedisStorageContainer = “Use Redis as storage”
So for Prefect results/cache, I’d still lean RedisStorageContainer.
## Rule of thumb
- Need result_storage=? → use RedisStorageContainer
- Need get_client() and raw Redis commands? → use RedisDatabase
If you want, I can give you:
1. a working example using only RedisDatabase since that’s what you already have in the UI, or
2. the exact commands/steps to register RedisStorageContainer in your Helm deployment.Russell Brooks
05/07/2026, 3:08 PMMarvin
05/07/2026, 3:08 PMMarvin
05/07/2026, 3:09 PMprefect-redis-master in namespace prefect, the FQDN is usually:
text
prefect-redis-master.prefect.svc.cluster.local
Format:
text
<service-name>.<namespace>.svc.cluster.local
## Best way to find it
From your machine, run:
bash
kubectl get svc -n <namespace>
Example:
bash
kubectl get svc -n prefect
Then take the Redis service name and build:
text
<service-name>.prefect.svc.cluster.local
## If you want to inspect from inside the pod
You can exec into the Redis pod and check DNS-related info, but note this only helps confirm namespace/search domains.
Useful command:
bash
cat /etc/resolv.conf
For example:
bash
kubectl exec -it -n prefect <redis-pod-name> -- cat /etc/resolv.conf
That will show the search domains, often including something like:
text
search prefect.svc.cluster.local svc.cluster.local cluster.local
From that, you can confirm the namespace and cluster domain.
## If DNS tools are installed in the pod
You can try one of these:
bash
hostname -f
or
bash
nslookup <service-name>
or
bash
getent hosts <service-name>
For example:
bash
kubectl exec -it -n prefect <redis-pod-name> -- nslookup prefect-redis-master
But hostname -f gives the pod hostname, not the service FQDN, so for your use case the service name is what matters.
## Recommended approach
1. Get the namespace:
bash
kubectl get pods -A | grep redis
2. Get the service:
bash
kubectl get svc -n <namespace>
3. Build the FQDN:
text
<service>.<namespace>.svc.cluster.local
## Example
If:
- namespace = prefect
- service = prefect-redis-master
then use:
text
prefect-redis-master.prefect.svc.cluster.local
## One more note
If your flow runs in the same namespace, you can often just use:
text
prefect-redis-master
or sometimes:
text
prefect-redis-master.prefect
and Kubernetes DNS will resolve it.
If you want, I can help you figure out the exact Redis service name from your prefect-helm release naming pattern.Russell Brooks
05/07/2026, 3:31 PMMarvin
05/07/2026, 3:31 PMMarvin
05/07/2026, 3:31 PMRussell Brooks
05/07/2026, 3:33 PMMarvin
05/07/2026, 3:33 PMMarvin
05/07/2026, 3:35 PMRedisStorageContainer writes out of the box.
RedisStorageContainer.write_path(...) just does a Redis SET without an expiration, so there’s no built-in ttl= or ex= argument.
## What is supported?
### 1. Prefect cache expiration
If your goal is task caching, the supported Prefect-native way is:
python
from datetime import timedelta
from prefect import task
@task(
persist_result=True,
cache_expiration=timedelta(hours=1),
)
def my_task():
...
This means:
- Prefect will treat the cached result as expired after 1 hour
- after that, the task runs again instead of reusing the cached result
Important: this is Prefect cache expiration logic, not Redis key TTL.
So the Redis key may still physically exist, but Prefect won’t reuse it after expiration.
## Example with Redis result storage + cache expiration
python
from datetime import timedelta
from prefect import flow, task
from prefect.cache_policies import INPUTS
from prefect.blocks.redis import RedisStorageContainer
from pydantic import SecretStr
redis_storage = RedisStorageContainer(
host="prefect-redis-master.prefect.svc.cluster.local",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
)
@task(
persist_result=True,
result_storage=redis_storage,
cache_policy=INPUTS,
cache_expiration=timedelta(hours=1),
)
def expensive_task(x: int) -> int:
print(f"computing {x}")
return x * 10
@flow
def my_flow():
a = expensive_task(5)
b = expensive_task(5)
return a, b
Behavior:
- second call can reuse the cached result
- after 1 hour, Prefect considers that cache entry expired
- the task executes again
## If you want actual Redis key expiration
You’d need one of these approaches:
### Option A: custom subclass
Subclass RedisStorageContainer and override write_path / awrite_path to call Redis SET with ex=<seconds>.
Conceptually:
python
from pathlib import Path
import redis
from prefect.blocks.redis import RedisStorageContainer
class TTLRedisStorageContainer(RedisStorageContainer):
ttl_seconds: int = 3600
def write_path(self, path: Path | str, content: bytes) -> bool:
client = redis.Redis(
host=self.host,
port=self.port,
db=self.db,
username=self.username.get_secret_value() if self.username else None,
password=self.password.get_secret_value() if self.password else None,
)
try:
return client.set(str(path), content, ex=self.ttl_seconds)
finally:
client.close()
That gives you real Redis TTLs, but it’s a custom solution rather than built-in Prefect behavior.
### Option B: external Redis policy / cleanup
Use:
- Redis eviction settings
- a cleanup job
- Redis EXPIRE applied externally
This is more infrastructure-driven than Prefect-driven.
## Which should you use?
If your goal is:
### “Don’t reuse stale cached results”
Use cache_expiration.
If your goal is:
### “I need Redis to physically delete keys after N seconds”
Use:
- a custom storage block subclass, or
- Redis-side cleanup/TTL strategy
## Key distinction
- cache_expiration = Prefect decides whether a cached result is still valid
- Redis TTL = Redis deletes the key automatically
Those are related, but not the same.
## Recommendation
For most Prefect use cases, I’d do this:
- result_storage=RedisStorageContainer(...)
- persist_result=True
- cache_policy=INPUTS or another policy
- cache_expiration=timedelta(...)
That gives you the behavior most people want without customizing storage internals.
Docs:
- Results: Prefect results docs
- Caching: Prefect task caching docs
- prefect-helm: prefect-helmMarvin
05/07/2026, 3:35 PMTTLRedisStorageContainer implementation you could register and use as a block.Russell Brooks
05/07/2026, 3:38 PMMarvin
05/07/2026, 3:38 PMMarvin
05/07/2026, 3:41 PMcache_expiration for Prefect cache semantics
- use Redis TTL only when you explicitly want Redis to be an ephemeral result/cache store
- choose a TTL that is comfortably longer than the time between write and expected read/retry/reinspection
That said, here is a production-oriented custom block.
## What this custom block does
- subclasses RedisStorageContainer
- adds default_ttl_seconds
- applies Redis SET ... EX <ttl> on every write
- supports both sync and async writes
- validates config
- uses logging
- supports either host/port/db or connection string
- is usable as Prefect result_storage
## Production-ready TTLRedisStorageContainer
Put this in a module that is available to your workers, for example `custom_redis_storage.py`:
```python
from future import annotations
import logging
from pathlib import Path
from typing import Optional
import redis
from pydantic import Field, model_validator
from pydantic.types import SecretStr
from prefect.blocks.redis import RedisStorageContainer
from prefect._internal.compatibility.async_dispatch import async_dispatch
logger = logging.getLogger(name)
class TTLRedisStorageContainer(RedisStorageContainer):
"""
Redis-backed Prefect result storage with Redis-native TTL on writes.
Important:
- Expired keys are physically removed by Redis.
- If Prefect later tries to read an expired result, it will not be available.
- Best suited for ephemeral result storage / caching, not long-lived durable results.
"""
_block_type_name = "TTL Redis Storage Container"
_description = "Redis storage container with native Redis key expiration (TTL) on writes."
default_ttl_seconds: Optional[int] = Field(
default=None,
description=(
"Default TTL in seconds for keys written to Redis. "
"If null, keys do not expire."
),
)
log_missing_keys: bool = Field(
default=True,
description=(
"If true, log when a key is missing on read. "
"This often indicates Redis TTL expiration."
),
)
@model_validator(mode="after")
def validate_ttl(self) -> "TTLRedisStorageContainer":
if self.default_ttl_seconds is not None and self.default_ttl_seconds <= 0:
raise ValueError("default_ttl_seconds must be a positive integer or null")
if self.default_ttl_seconds is not None and self.default_ttl_seconds < 60:
logger.warning(
"TTLRedisStorageContainer configured with a very short TTL "
"(%s seconds). Results may expire before downstream reads occur.",
self.default_ttl_seconds,
)
return self
def _get_sync_client(self) -> redis.Redis:
if self.connection_string:
return redis.Redis.from_url(
self.connection_string.get_secret_value(),
decode_responses=False,
)
if not self.host:
raise ValueError("Either connection_string or host must be set")
return redis.Redis(
host=self.host,
port=self.port,
db=self.db,
username=self.username.get_secret_value() if self.username else None,
password=self.password.get_secret_value() if self.password else None,
decode_responses=False,
)
async def awrite_path(self, path: Path | str, content: bytes) -> bool:
"""
Write bytes to Redis with TTL if configured.
"""Marvin
05/07/2026, 3:41 PM## How to create and save the block
Example:
```python
from pydantic import SecretStr
from custom_redis_storage import TTLRedisStorageContainer
block = TTLRedisStorageContainer.from_host(
host="prefect-redis-master.prefect.svc.cluster.local",
port=6379,
db=1,
password=SecretStr("my-redis-password"),
default_ttl_seconds=7 * 24 * 60 * 60, # 7 days
)
block.save("redis-results-ttl-7d", overwrite=True)
## How to use it in a flow
```python
from prefect import flow, task
from prefect.cache_policies import INPUTS
from custom_redis_storage import TTLRedisStorageContainer
redis_storage = TTLRedisStorageContainer.load("redis-results-ttl-7d")
@task(
persist_result=True,
result_storage=redis_storage,
cache_policy=INPUTS,
)
def expensive_task(x: int) -> int:
print(f"computing {x}")
return x * 100
@flow(
persist_result=True,
result_storage=redis_storage,
)
def my_flow():
a = expensive_task(5)Marvin
05/07/2026, 3:41 PM## Recommended production settings
I would *not* default this to a tiny TTL.
Good starting points:
- *1 day* for very ephemeral cache-style workloads
- *7 days* for most result browsing / retries / debugging
- *14-30 days* if you use Redis results operationally and still want cleanup
Example:
```python
default_ttl_seconds=604800 # 7 days
## Very important caveats
### 1. Expired results are gone for real
Unlike cache_expiration, Redis TTL means the key is physically deleted.
That means:
- retries or downstream reads may fail if the result expired
- old flow/task results may no longer be restorable
- UI/API access to persisted results may be incomplete
### 2. This is better for cache-like results than archival results
If you want durable persisted results, object storage is usually a better fit:
- S3
- GCS
- Azure Blob
- filesystem/PVC
Redis is best when you want:
- speed
- shared temporary storage
- bounded retention
### 3. Pair it with cache_expiration
If this is being used for task caching, still set Prefect cache expiration explicitly.
Example:
python
from datetime import timedelta
from prefect import task
from prefect.cache_policies import INPUTS
@task(
persist_result=True,
cache_policy=INPUTS,
cache_expiration=timedelta(days=7),
result_storage=redis_storage,
)
def my_task(x: int) -> int:
return x * 2
That way:
- Prefect knows when a cache is logically stale
- Redis also eventually removes old keys
## Registering / making it available
For this custom block to work smoothly:
### Worker/runtime requirements
Your worker image/environment needs:
- your custom_redis_storage.py module
- prefect
- redis
### If you want it to show in the UI
Import it and save an instance. If you want explicit registration behavior, you can also ensure the module is imported in the environment where you create the block.
A simple script:
python
from custom_redis_storage import TTLRedisStorageContainer
# importing the class and saving a block document is usually enough
block = TTLRedisStorageContainer.from_connection_string(
"redis://:my-redis-password@prefect-redis-master.prefect.svc.cluster.local:6379/1",
default_ttl_seconds=604800,
)
block.save("redis-results-ttl-7d", overwrite=True)
## My honest recommendation
I agree with your intuition that people often want TTL for Redis-backed storage, because otherwise Redis can fill up.
But I would frame this as:
- great for Redis as ephemeral result storage / cache storage
- not automatically the right default for all Prefect results
For many teams, the best setup is:
- Redis for fast ephemeral cache
- object storage for durable results
## Relevant docs
- Results: Prefect results docs
- Caching: Prefect task caching docs
- prefect-helm: prefect-helm
If you want, I can next give you:
- a Helm/Docker image example showing how to package this custom block into your worker image, or
- a safer hybrid design where Redis gets TTL but only for task cache, while durable flow results go somewhere else.