Last updated 2026-09-04Development preview

Cache replaceable data

Understand hits, misses, expiry, and errors, then store values through an action-scoped cache client.


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.

ResultMeaningApplication behavior
found: trueAn entry exists, even if its value is emptyUse the value if it is acceptable for this request
found: falseThe entry is absent, expired, or removedRead the authoritative source and optionally refill the cache
RPC/provider errorThe lookup did not complete successfullyFollow 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:

yaml
caches:
  catalog: {}
runtimes:
  api:
    uses:
      caches:
        - name: catalog
          actions: [get, set, delete]

Workspace policy selects the cloud product:

yaml
resourceTypes:
  caches:
    valkey:
      product: gcp.memorystore.valkey
environments:
  prod:
    caches:
      defaults:
        type: valkey
        provider: application

Add 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 typescript

Use 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

ItemSupported behavior
getGenerates Get; inspect found separately from value length
setGenerates Set; writes the key's value and selected expiry
deleteGenerates Delete; deleted reports whether an entry was removed
Key size1–1,024 bytes
Value size0–524,288 bytes
expiresInSecondsInteger from 0 to 31,536,000 seconds
Zero expiryNo 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.