Cache-Aside Pattern
How to use cacheGet for cache-aside reads — API, TTL defaults, serialization, and usage examples.
How cacheGet Works
The cacheGet<T> function implements the cache-aside (lazy-loading) pattern:
- Check Redis for the given key.
- If the key exists, deserialize the JSON and return it.
- If the key is missing, call the
fetcherfunction to compute the value. - Serialize the result as JSON and store it in Redis with a TTL.
- Return the computed value.
import { cacheGet } from "@repo/cache";
const org = await cacheGet(
`tenant:${orgId}:settings`,
async () => {
// This only runs on cache miss
return db.query.organizations.findFirst({
where: eq(organizations.id, orgId),
});
},
600, // TTL in seconds (10 minutes)
);
Signature
function cacheGet<T>(
key: string,
fetcher: () => Promise<T>,
ttlSeconds?: number, // default: 300 (5 minutes)
): Promise<T>;
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key | string | — | The cache key (see Key Conventions) |
fetcher | () => Promise<T> | — | Async function that computes the value on miss |
ttlSeconds | number | 300 | Time-to-live in seconds |
TTL Guidelines
| Data type | Suggested TTL | Rationale |
|---|---|---|
| Tenant settings | 600s (10 min) | Rarely changes, safe to serve slightly stale |
| Reference data (enums) | 3600s (1 hr) | Almost never changes after deployment |
| User profile | 300s (5 min) | Default — balances freshness with performance |
| Search results | 60-120s | Results change as content is indexed |
| Rate-limit counters | Window size | TTL matches the rate-limit window exactly |
These are suggestions. Choose the shortest TTL that still provides meaningful cache hit rates for your use case.
Serialization
Values are stored as JSON via JSON.stringify / JSON.parse. This means:
- Primitive types, plain objects, and arrays work out of the box.
Dateobjects are serialized as ISO strings — you must reconstruct them on read if needed.- Circular references will throw at write time.
undefinedvalues in objects are silently dropped (standard JSON behavior).Map,Set,BigInt, and class instances are not supported without custom serialization.
If you need to cache a value with non-JSON-safe types, transform it before caching:
const result = await cacheGet(
`tenant:${orgId}:metrics:${period}`,
async () => {
const raw = await computeMetrics(orgId, period);
// Dates are serialized as ISO strings — this is fine if consumers expect strings
return raw;
},
300,
);
Error Behavior
- If Redis is unreachable,
getRedis()will attempt the connection (up to 3 retries). If all retries fail, the error propagates to the caller. The fetcher is never called. - If the fetcher throws, the error propagates to the caller. Nothing is written to the cache.
- If the JSON parse fails on a cached value (data corruption), the error propagates. Consider wrapping
cacheGetin a try/catch if you need fallback-to-fetcher behavior on corrupt cache entries.
There is no built-in stale-while-revalidate or circuit-breaker pattern. If Redis is down, requests that use cacheGet will fail unless you add your own error handling.
When Not to Use cacheGet
- Write-heavy data that changes on every request — the cache hit rate will be near zero.
- Data that must be real-time consistent — even a 5-second TTL introduces staleness.
- Large payloads (> 1 MB) — Redis stores everything in memory; large values waste Upstash quota.
- Sensitive data (tokens, passwords, PII) — unless you have a clear TTL and invalidation strategy. The Redis instance is shared across the application.
For these cases, query the database directly or use a different caching strategy.