Skip to Content
DevelopRead-only Methods

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 the most important part of that contract — but not all of it. It is essential to understand exactly what is and isn’t checked.

Enforced: durable writes, HTTP, and RPC trap

The following all go through Golem’s durability layer, and a read-only method traps immediately on any of them — before the call runs and before any change is persisted. The invocation fails with a typed ReadOnlyViolation that identifies the agent method and offending host function.

  • writes through Golem’s durable host APIs (key-value and blob storage, external databases, promises, agent management, …),
  • making outgoing HTTP requests,
  • calling other agents over RPC.

Not enforced: your responsibility

Golem does not detect the following. They will not trap, but they silently break the read-only contract — either by mutating state that should be immutable here, or by making the result non-deterministic — which corrupts the cache. You must avoid them yourself:

  • Mutating the agent’s in-memory state (assigning to a field). This is a plain in-memory write, not a host call, so there is nothing for the runtime to intercept. It does not trap — but it must never be done in a read-only method.
  • Changing the agent’s own files (creating, writing, or deleting them). Filesystem permissions still apply, but the read-only marker does not block these operations.
  • Reading the clock / current time.
  • Using randomness.
  • Reading environment variables.
  • Remote / blob reads.

The runtime rejects durable write calls, outgoing HTTP, and RPC from read-only methods. It does not prevent mutation of the agent’s own memory or files, or reject non-deterministic reads (time, randomness, environment, remote reads). Keeping a read-only method pure is the author’s responsibility.

In exchange for honoring this contract, Golem provides:

GuaranteeWhat it means
No writes / external callsWrites, outgoing HTTP, and RPC trap with a typed ReadOnlyViolation error, before anything is written.
Cached resultThe result is cached per (method, normalized input, optional principal) on the worker.
Queue & loading bypassA 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. No durable write entries are produced, because durable write calls trap before they can append to the oplog.

Cache policies

The cache policy is part of the read-only marker — non-read-only methods cannot carry one.

PolicyBehavior
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-cacheThe 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 (automatic)

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. Rust, Scala, MoonBit, and Effect TypeScript detect it and automatically mark the read-only configuration as principal-dependent (uses_principal = true). Plain TypeScript requires readOnly: { usesPrincipal: true }, as shown below. The calling principal then 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.

What works in a read-only method

The “not allowed” operations fall into two groups: those the runtime traps on, and those it does not check but which still break the contract and must be avoided by you.

AllowedNot allowed — traps with ReadOnlyViolationNot allowed — not checked, your responsibility
Reading the agent’s state fieldsWriting through durable host APIs (storage, external databases, …)Mutating in-memory state (assigning to a field)
Computation over the method inputsOutgoing HTTP requestsReading the clock / current time
Returning derived/aggregated valuesRPC calls to other agentsRandomness
Reading environment variables
Remote / blob reads
Creating, writing, or deleting the agent’s own files

Everything in the middle column traps before it runs. Everything in the right column runs silently — it will not trap, but it corrupts the cache, so you must avoid it yourself. If you need any of these operations, use a regular (non-read-only) method instead.

Marking a method read-only

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; per-principal caching is enabled with usesPrincipal: true:

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: {}, returns: z.array(Item), readOnly: { usesPrincipal: 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. (A read-only method itself cannot make an RPC call — that would trap.)
  • 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 policyResponse headers
no-cacheCache-Control: no-store
until-writeCache-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.