<@ULVA73B9P> in kubernetes self hosted prefect I w...
# ask-marvin
r
@Marvin in kubernetes self hosted prefect I want to reuse the same redis app as generated by the prefect-helm chart for purposes of result storage and cache. Can you give a small example of how to set that up? Presumably I'll need to set up a Block? What else?
m
Error: unhandled errors in a TaskGroup (1 sub-exception)
Sorry, I encountered an error while processing your request. Please try again.
r
@Marvin can you explain and show an example of how to use the redis sub chart from prefect-helm as a result storage and for cache?
m
thought for 208.3 seconds
Yep — you can use the Redis instance created by the
prefect-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:
Copy code
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:
Copy code
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:
Copy code
text
<helm-release-name>-redis-master
So if your Helm release is named
prefect
, the host is often:
Copy code
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.:
Copy code
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
Copy code
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
Copy code
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 key
- returning the saved result instead of re-running the task So task caching depends on persisted results. If you want Redis-backed cache behavior, use: -
persist_result=True
- a cache policy like
INPUTS
- Redis as the task/flow result storage Example:
Copy code
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:
Copy code
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:
Copy 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",
port=6379, db=1, password=SecretStr("my-redis-password"), ) @task( persist_result=True, cache_policy=INPUTS, result_storage=redis_storage, ) def slow_add(x: int, y: int) -> int: print(f"computing {x} + {y}") return x + y @flow(persist_result=True, result_storage=redis_storage) def demo(): r1 = slow_add(1, 2) r2 = slow_add(1, 2) r3 = slow_add(2, 3) return r1, r2, r3 if name == "__main__": print(demo()) ``` ## 13. Docs / references Useful links: - Prefect Helm chart repo: prefect-helm - Prefect Helm docs: prefect-server chart - Prefect Redis collection: prefect-redis - Prefect result persistence docs: results documentation - Prefect caching docs: task caching documentation If you want, I can also show: - a Helm values.yaml + deployment example for this setup, or - a version that uses Kubernetes secrets/env vars instead of hardcoding the Redis password.
r
@Marvin I'm confused between the redisdatabase block and the redisstoragecontainer block you mention. Also I only see redisdatabase in my ui.
m
thought for 155.8 seconds
Yes — that makes sense, and the distinction is a bit confusing. Short answer: -
RedisStorageContainer
= 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:
Copy code
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:
Copy code
python
@flow(result_storage=redis_storage, persist_result=True)
def my_flow():
    ...
###
RedisDatabase
Import path:
Copy code
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:
Copy code
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:
Copy code
python
from prefect.blocks.redis import RedisStorageContainer
Then run:
Copy code
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 UI
- you just want Redis-backed result storage/caching working ### Option B: register
RedisStorageContainer
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.
r
@Marvin I struggle with the host name. Which command can I run in the redis pod to find the fully qualified domain name?
m
thought for 12.9 seconds
You usually don’t need to exec into the Redis pod to discover the FQDN — the fully qualified DNS name in Kubernetes is based on the Service, not the Pod. For a Redis service named
prefect-redis-master
in namespace
prefect
, the FQDN is usually:
Copy code
text
prefect-redis-master.prefect.svc.cluster.local
Format:
Copy code
text
<service-name>.<namespace>.svc.cluster.local
## Best way to find it From your machine, run:
Copy code
bash
kubectl get svc -n <namespace>
Example:
Copy code
bash
kubectl get svc -n prefect
Then take the Redis service name and build:
Copy code
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:
Copy code
bash
cat /etc/resolv.conf
For example:
Copy code
bash
kubectl exec -it -n prefect <redis-pod-name> -- cat /etc/resolv.conf
That will show the search domains, often including something like:
Copy code
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:
Copy code
bash
hostname -f
or
Copy code
bash
nslookup <service-name>
or
Copy code
bash
getent hosts <service-name>
For example:
Copy code
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:
Copy code
bash
kubectl get pods -A | grep redis
2. Get the service:
Copy code
bash
kubectl get svc -n <namespace>
3. Build the FQDN:
Copy code
text
<service>.<namespace>.svc.cluster.local
## Example If: - namespace =
prefect
- service =
prefect-redis-master
then use:
Copy code
text
prefect-redis-master.prefect.svc.cluster.local
## One more note If your flow runs in the same namespace, you can often just use:
Copy code
text
prefect-redis-master
or sometimes:
Copy code
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.
r
@Marvin ok. And how to add a time to live for the result?
m
Error: unhandled errors in a TaskGroup (1 sub-exception)
Sorry, I encountered an error while processing your request. Please try again.
r
@Marvin for the redisstoragecontainer to be used for prefect result storage, how can I add a ttl, also known as time to live?
m
thought for 126.9 seconds
Short answer: you can’t set a Redis TTL directly on
RedisStorageContainer
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:
Copy code
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
Copy code
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:
Copy code
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-helm
If you want, I can show a production-ready custom
TTLRedisStorageContainer
implementation you could register and use as a block.
r
@Marvin yes make a production ready TTLRedisStorageContainer please. I can imagine everyone user would like the choice to add ttl otherwise redis will get too full.
m
thought for 156.3 seconds
Yes — and I should give you one with an important warning up front: Adding Redis TTL at the storage layer is useful, but it changes failure behavior. If a result expires in Redis before Prefect reads it back, then Prefect may no longer be able to retrieve that result. That is fine if you want ephemeral cache-like storage, but it is not ideal for durable flow results you may inspect later in the UI. So the safest production recommendation is: - use
cache_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. """
async with self._client() as client: return await client.set( str(path), content, ex=self.default_ttl_seconds, ) @async_dispatch(awrite_path) def write_path(self, path: Path | str, content: bytes) -> bool: """ Synchronous write with TTL if configured. """ client = self._get_sync_client() try: return client.set( str(path), content, ex=self.default_ttl_seconds, ) finally: client.close() async def aread_path(self, path: Path | str) -> bytes | None: """ Read bytes from Redis. Returns None if the key does not exist, which may mean: - the key never existed - the key expired due to TTL - the key was deleted externally """ async with self._client() as client: content = await client.get(str(path)) if content is None and self.log_missing_keys: logger.info( "Redis key '%s' was not found. It may have expired due to TTL.", path, ) return content @async_dispatch(aread_path) def read_path(self, path: Path | str) -> bytes | None: """ Synchronous read. """ client = self._get_sync_client() try: content = client.get(str(path)) finally: client.close() if content is None and self.log_missing_keys: logger.info( "Redis key '%s' was not found. It may have expired due to TTL.", path, ) return content @classmethod def from_host( cls, host: str, port: int = 6379, db: int = 0, username: Optional[str | SecretStr] = None, password: Optional[str | SecretStr] = None, default_ttl_seconds: Optional[int] = None, log_missing_keys: bool = True, ) -> "TTLRedisStorageContainer": if isinstance(username, str): username = SecretStr(username) if isinstance(password, str): password = SecretStr(password) return cls( host=host, port=port, db=db, username=username, password=password, default_ttl_seconds=default_ttl_seconds, log_missing_keys=log_missing_keys, ) @classmethod def from_connection_string( cls, connection_string: str | SecretStr, default_ttl_seconds: Optional[int] = None, log_missing_keys: bool = True, ) -> "TTLRedisStorageContainer": if isinstance(connection_string, str): connection_string = SecretStr(connection_string) return cls( connection_string=connection_string, default_ttl_seconds=default_ttl_seconds, log_missing_keys=log_missing_keys, )
Copy code
## 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)
b = expensive_task(5) return a, b
Copy code
## 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:
Copy code
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:
Copy code
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.