Read-only agent methods
Golem lets agent authors mark individual agent methods as read-only. A read-only method is one you promise performs only a pure read of the agent’s state, and Golem caches its result, so read-heavy methods can be served without waiting behind slow writes — and without even loading the agent when a cached value is available.
This page explains what the runtime actually enforces (and, just as importantly, what it does not), how the cache policies work, and how to use the marker from each SDK.
What “read-only” means
A read-only method must be a pure function of the agent’s already-loaded state and the method inputs: it must not change anything, and its result must depend only on its inputs and the current state. That is the contract you are promising.
Golem enforces only part of this contract:
| Allowed | Not allowed — runtime-enforced | Not allowed — author’s responsibility |
|---|---|---|
| Reading the agent’s state fields | Writing through durable host APIs (key-value and blob storage, external databases, promises, agent management, …) | Mutating in-memory state (assigning to a field) |
| Computation over the method inputs | Outgoing HTTP requests | Reading the clock / current time |
| Returning derived/aggregated values | RPC calls to other agents | Randomness |
| Reading environment variables | ||
| Remote / blob reads | ||
| Creating, writing, or deleting the agent’s own files |
Operations in the middle column trap before the call runs or any change is persisted. The invocation
fails with a typed ReadOnlyViolation that identifies the agent method and offending host function.
Golem does not detect the operations in the right column: in-memory writes are not host calls, and
filesystem permissions do not enforce the read-only contract. These operations can make cached
results incorrect, so you must avoid them yourself. If you need them, use a regular (non-read-only) method.
In exchange for honoring this contract, Golem provides:
| Guarantee | What it means |
|---|---|
| Cached result | The result is cached per (method, normalized input, optional principal) on the worker. |
| Queue & loading bypass | A cache hit returns immediately — it never acquires the agent’s invocation queue and never loads an evicted agent. |
A cache hit writes nothing to the oplog and does not load the agent. A cache miss (or a no-cache
method) still runs through the queue and writes the usual AgentInvocationStarted / AgentInvocationFinished
markers — needed for oplog visibility, fuel accounting, and idempotency-key result lookup.
Cache policies
The cache policy is part of the read-only marker — non-read-only methods cannot carry one.
| Policy | Behavior |
|---|---|
until-write (default) | The cached result is valid until the next non-read-only invocation on the same agent invalidates it. |
ttl(d) | The cached result expires after duration d, even if no write happened in the meantime. |
no-cache | The method runs every time without caching; the read-only contract still applies. |
After a successful non-read-only invocation commits its completion marker, Golem invalidates cached read-only
results for that agent. While a mutation is queued or running, readers can still receive the previous cached
value. Updating or reverting the agent also invalidates its cache; evicting and reloading only the WebAssembly
instance does not. ttl(d) entries additionally expire by time. no-cache never inserts into the cache.
Per-principal caching
By default a read-only result is shared across all callers — the cache key is (method, input) and HTTP
responses are marked Cache-Control: public (CDN-friendly).
When the result depends on who is calling (for example, an authorization-scoped view), add a Principal
parameter to the method and enable per-principal caching:
TypeScript
Add a direct s.principal() input to the method. The TypeScript SDK automatically enables
per-principal caching.
With per-principal caching enabled, the calling principal becomes part of the cache key, so each principal gets
its own cached entry, and HTTP responses switch to Cache-Control: private. The response’s Vary header names
the configured authentication header and any request headers bound as method inputs.
The Principal parameter is marked as auto-injected in the method’s input schema. The runtime supplies it;
callers do not pass it.
Read-only and ephemeral are mutually exclusive
Read-only methods read the agent’s shared, already-loaded state. Ephemeral agents get a fresh instance per invocation and have no shared state to read from, so a read-only method on an ephemeral agent has no meaning. The host rejects this combination when the agent registers its types; individual SDKs may also reject it earlier.
Marking a method read-only
TypeScript
Mark a method read-only by setting readOnly: true on its method(...) spec:
import { z } from "zod"
import { defineAgent, method } from "@golemcloud/golem-ts-sdk"
export const CounterAgent = defineAgent({
name: "CounterAgent",
id: { name: z.string() },
methods: {
// Non-read-only: writes shared state
increment: method({ input: {}, returns: z.number() }),
// Read-only: pure read over already-loaded state
getCount: method({ input: {}, returns: z.number(), readOnly: true }),
},
})
export const CounterAgentImpl = CounterAgent.implement({
init: () => ({ count: 0 }),
methods: {
increment() {
this.count += 1
return this.count
},
getCount() {
return this.count
},
},
})readOnly: true uses the default until-write cache policy. For finer control, pass an object instead of
the boolean. A direct s.principal() input automatically enables per-principal caching:
methods: {
// TTL cache: the result expires 30s after it is computed
recentSummary: method({
input: {},
returns: Summary,
readOnly: { cache: { ttlNanos: 30_000_000_000n } },
}),
// Never cached, but still side-effect-free
pureCompute: method({
input: { x: z.number(), y: z.number() },
returns: z.number(),
readOnly: { cache: "no-cache" },
}),
// Principal-aware: cached per caller principal
myVisibleItems: method({
input: { caller: s.principal() },
returns: z.array(Item),
readOnly: true,
}),
}Performance contract
- Queue bypass. On a cache hit the result is returned without acquiring the agent’s invocation queue, so a read-only call is not blocked behind a slow write that is currently being processed.
- Agent-loading bypass. The cache lives on the worker independently of whether the WebAssembly instance is loaded. A cache hit is served even when the agent is currently evicted — no instance loading happens.
- Automatic RPC fast path. The cache lookup sits at the single invocation entry point shared by the public API and agent-to-agent RPC, so an RPC into a read-only method on the same node hits the cache too.
- Concurrent coalescing. Many identical first-time reads coalesce into a single execution; the rest wait for and share that one result.
HTTP cache headers
When a read-only method is mapped to an HTTP GET or HEAD endpoint (see
Exposing agents over HTTP), Golem emits cache-friendly response headers derived from the
cache policy and whether the method is principal-dependent:
| Cache policy | Response headers |
|---|---|
no-cache | Cache-Control: no-store |
until-write | Cache-Control: <visibility>, no-cache + an opaque ETag |
ttl(d) | Cache-Control: <visibility>, max-age=<d> + an opaque ETag |
Where <visibility> is private when the method is principal-dependent, and public otherwise. Vary always
names request headers bound as inputs; for principal-dependent methods it also names the configured authentication
header.
A request that repeats the returned ETag in If-None-Match is revalidated cheaply. If it still matches the
agent’s current agent-instance fingerprint and oplog position, Golem returns 304 Not Modified without executing
the agent method or loading its WebAssembly instance.
A response a CDN or browser sees for a public, until-write method therefore looks like:
HTTP/1.1 200 OK
Cache-Control: public, no-cache
ETag: "<opaque-etag>"
Content-Type: application/json
{"count":7}and a later conditional request short-circuits to:
GET /counter-1/count HTTP/1.1
If-None-Match: "<opaque-etag>"
HTTP/1.1 304 Not Modified
Cache-Control: public, no-cache
ETag: "<opaque-etag>"Read-only methods bound to non-GET/HEAD verbs are allowed, but they do not get cache headers — caching a
POST/PUT/DELETE response is not meaningful. Deployment validation emits a warning for each such binding.