Managed Cache
Zenifra's Cache component currently uses Valkey and is designed for values that can be rebuilt from a source of truth. Storage is disposable: restarts, expiration, and eviction may remove entries without representing application data loss.
Cache-aside pattern
- Read the key from Valkey.
- On a miss, query the source of truth.
- Store the result with a TTL and, when possible, a small jitter.
- Invalidate the key when the source of truth changes.
GET catalog:product:42
SET catalog:product:42 <json> EX 300
DEL catalog:product:42Never store tokens, passwords, or regulated data without an appropriate protection policy. Use namespaces, limit value sizes, and monitor hit/miss rates.
Node.js and TypeScript
npm install iovalkeyimport Redis from 'iovalkey'
import { randomInt } from 'node:crypto'
async function loadProductFromSource() {
return { id: 42, name: 'example' }
}
const url = new URL(process.env.VALKEY_URL ?? '')
const client = new Redis({
host: url.hostname,
port: Number(url.port),
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
tls: { servername: url.hostname },
})
const key = 'catalog:product:42'
const cached = await client.get(key)
const value = cached ?? JSON.stringify(await loadProductFromSource())
if (!cached) await client.set(key, value, 'EX', 300 + randomInt(0, 30))
console.log(value)
await client.quit()Replace loadProductFromSource() with the application’s real query. The example uses jitter to reduce simultaneous expirations; at high volume, combine it with a short lock or request coalescing.
Python
python -m pip install valkeyimport json
import os
import random
from urllib.parse import urlparse
from valkey import Valkey
def load_product_from_source():
return {"id": 42, "name": "example"}
url = urlparse(os.environ["VALKEY_URL"])
client = Valkey(
host=url.hostname, port=url.port, username=url.username, password=url.password,
ssl=True, ssl_check_hostname=True, decode_responses=True,
)
key = "catalog:product:42"
cached = client.get(key)
value = cached or json.dumps(load_product_from_source())
if cached is None:
client.set(key, value, ex=300 + random.randint(0, 30))
print(value)
client.close()Replace load_product_from_source() with the application query. To avoid a cache stampede, only one worker should rebuild an expiring key; others can wait or serve stale data for a short window.
What not to do
- Do not treat Cache as permanent storage.
- Do not use infinite TTLs for data that can grow without bounds.
- Do not use
KEYS *to clear the cache; use namespaces andSCANor key invalidation. - Do not assume a hit means that data is current; define an invalidation policy.
Safe operation
Choose TTLs according to how quickly the source changes, not according to plan size. A very short TTL increases load and latency; a very long TTL serves stale data. Random jitter prevents thousands of keys from expiring in the same second. When rebuilding is expensive, use a short token-based lock and allow readers to receive stale data during a controlled window.
Monitor hit rate, miss rate, latency, serialization errors, and eviction count. More misses can indicate excessive invalidation or insufficient capacity, while a high hit rate does not prove that content is correct. Define entity-level invalidation after writes to the source, and treat Cache failure as a normal path: the application should read the source and continue without disposable data.