A cache keeps a value that your application can obtain or compute again. Reading that value may be cheaper than repeating a database query or computation, but the application must still know what to do when it is absent.
Start with that fallback. If losing the value would lose the only copy of important data, it belongs in durable storage rather than only in this cache.
#Understand a cache lookup
A key identifies the cached value. Get returns both a found flag and bytes. An existing value can contain zero bytes, so an empty body does not prove a cache miss.
| Result | Meaning | Application behavior |
|---|---|---|
found: true | An entry exists, even if its value is empty | Use the value if it is acceptable for this request |
found: false | The entry is absent, expired, or removed | Read the authoritative source and optionally refill the cache |
| RPC/provider error | The lookup did not complete successfully | Follow your explicit cache-failure policy; do not silently call it a normal miss |
#Expiry bounds age, not correctness
A time-to-live limits how long a stored entry remains before expiry. If the underlying record changes sooner, the cached entry can still be stale. Choose an expiry based on what the application can tolerate, and invalidate or replace entries when your write workflow requires fresher data.
For example, a five-minute cached public catalog may be acceptable for browsing. The same cached result should not silently become the authority for an operation that needs current data.
#Access is still per runtime
get, set, and delete are separate grants. A reader does not automatically receive a setter. Fluffy generates a client for the named logical cache and routes it through the runtime's sidecar.
The portable cache API exposes these operations. It is not a general client for every command a native Valkey server understands.
#Configure a cache
#Declare the cache
Add this excerpt to fluffy-chainsaw.yaml, keeping the existing api runtime's other settings:
caches:
catalog: {}
runtimes:
api:
uses:
caches:
- name: catalog
actions: [get, set, delete]Workspace policy selects the cloud product:
resourceTypes:
caches:
valkey:
product: gcp.memorystore.valkey
environments:
prod:
caches:
defaults:
type: valkey
provider: applicationAdd the workspace excerpt alongside the configured application provider. Local execution supplies the local cache product. Cloud capacity and provider placement come from the resolved deployment plan; they are not inferred from the size of the local container.
#Set and retrieve a value
Generate the runtime SDK for your application's name; here it is example-app:
fluffy-chainsaw generate --application example-app --runtime api --language typescriptUse the sidecar setup for your selected language. These examples use example-app and module example.com/app; adjust the imports to your application. TypeScript uses address and transport; Python uses channel. Go passes its connection and a deadline-bearing context to the function:
import { Metadata } from '@grpc/grpc-js'
import * as runtimev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/runtime/example-app/api.js'
import * as cachev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/cache/v1/cache.js'
const client = new runtimev1.CachesCatalogClient(address, transport)
const stored = await new Promise<cachev1.SetResponse>((resolve, reject) => {
client.set(
cachev1.SetRequest.fromPartial({
key: Buffer.from('featured'),
value: Buffer.from('featured-item'),
expiresInSeconds: 300,
}),
new Metadata(),
{ deadline: Date.now() + 5000 },
(error, value) => {
if (error) reject(error)
else resolve(value)
},
)
})
const value = await new Promise<cachev1.GetResponse>((resolve, reject) => {
client.get(
cachev1.GetRequest.fromPartial({ key: Buffer.from('featured') }),
new Metadata(),
{ deadline: Date.now() + 5000 },
(error, value) => {
if (error) reject(error)
else resolve(value)
},
)
})
if (!value.found) throw new Error('verification entry is absent')
const deleted = await new Promise<cachev1.DeleteResponse>((resolve, reject) => {
client.delete(
cachev1.DeleteRequest.fromPartial({ key: Buffer.from('featured') }),
new Metadata(),
{ deadline: Date.now() + 5000 },
(error, value) => {
if (error) reject(error)
else resolve(value)
},
)
})The explicit miss error above checks a value this recipe just stored. In your request handler, a normal miss should usually fetch from the authoritative source and optionally store the result. Handle failures from that source too; the cache does not replace its error handling.
#Verify the lifecycle
Run the application locally. Set a known value, retrieve the same bytes, delete it, and confirm the next Get reports found: false. Store an empty value and confirm it reports found: true. Use a short expiry to check that an expired entry is treated as a miss.
Also exercise the application's fallback when the cache is unavailable. Decide whether each workflow may continue against its authoritative source or must fail, and make that decision visible in its error handling.
#Reference: cache operations and bounds
| Item | Supported behavior |
|---|---|
get | Generates Get; inspect found separately from value length |
set | Generates Set; writes the key's value and selected expiry |
delete | Generates Delete; deleted reports whether an entry was removed |
| Key size | 1–1,024 bytes |
| Value size | 0–524,288 bytes |
expiresInSeconds | Integer from 0 to 31,536,000 seconds |
| Zero expiry | No time-based expiration is set; this does not make the cache durable |
The size and expiry bounds are checked by the local and GCP cache adapters. Oversized values, empty keys, negative expiry, and expiry beyond the supported maximum are invalid requests. Unsupported or duplicate manifest actions are rejected before runtime use.
#Local and native behavior
Portable Get, Set, and Delete work through the local cache product. Local native Unwrap and Credential are not implemented. In GCP, native access refers to Memorystore; its credential is the runtime service account's OAuth token and may carry the union of that identity's IAM grants. See Native access before using it.
A cache miss is not a reason to expand permissions. A missing method is a grant or generation issue; an RPC failure needs its own diagnostic. Keeping these states separate makes the fallback predictable.
Continue with Messaging to hand work to another runtime instead of doing it inside the current request.