Marking Agent Methods as Read-Only (Rust)

SkillAI & models

Marking Rust agent methods as read-only for a side-effect-free guarantee and result caching. Use when the user wants a cacheable query method, a method that must not write to the oplog, or HTTP GET endpoints that emit cache headers.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Marking Agent Methods as Read-Only (Rust) skill

What this skill tells your AI

The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/rust/golem-mark-read-only-rust/SKILL.md and read by ahel’s review.

Overview

A read-only agent method is one you promise is a pure read of the agent's already-loaded state: it must not mutate anything and its result must depend only on its inputs and the current state. Golem enforces the most important part of this contract — writes to persistent state, outgoing HTTP, and RPC calls trap at runtime with AgentError::ReadOnlyViolation before they run — but it does not detect every source of impurity (in-memory mutation, clocks, randomness, env reads), so keeping the method pure is partly your responsibility (see What Works in a Read-Only Method). In exchange Golem:

  • Caches the result per (method, normalized input, optional principal) on the worker.
  • Bypasses the invocation queue on a cache hit — a read-only call returns immediately even while a slow write is being processed.
  • Bypasses agent loading on a cache hit — a cached value is served even if the agent is currently evicted.
  • Emits HTTP cache headers (Cache-Control / ETag / Vary) for read-only methods mapped to GET/HEAD.

Mark a method read-only with the #[read_only] attribute.

Usage

use golem_rust::{agent_definition, agent_implementation, read_only};

#[agent_definition]
pub trait CounterAgent {
    fn new(name: String) -> Self;

    // Non-read-only: writes shared state
    fn increment(&mut self) -> u64;

    // Read-only: pure read over already-loaded state
    #[read_only]
    fn get_count(&self) -> u64;
}

struct CounterAgentImpl {
    name: String,
    count: u64,
}

#[agent_implementation]
impl CounterAgent for CounterAgentImpl {
    fn new(name: String) -> Self {
        Self { name, count: 0 }
    }

    fn increment(&mut self) -> u64 {
        self.count += 1;
        self.count
    }

    fn get_count(&self) -> u64 {
        self.count
    }
}

Cache Policy

Pass cache = "..." to choose how long a cached result stays valid. The default is until_write.

PolicyAttributeBehavior
Until write (default)#[read_only] or #[read_only(cache = "until_write")]Cached until the next non-read-only invocation on the same agent
TTL#[read_only(cache = "ttl", ttl = "30s")]Expires after the given duration even without a write
No cache#[read_only(cache = "no_cache")]Runs every time; still side-effect-free, never cached

The ttl value is a humantime duration string (e.g. "30s", "5m", "2h"). The only valid #[read_only] arguments are cache and ttl.

#[read_only(cache = "ttl", ttl = "30s")]
fn recent_summary(&self) -> Summary;

#[read_only(cache = "no_cache")]
fn pure_compute(&self, x: u32, y: u32) -> u32;

Per-Principal Caching (automatic)

There is no uses_principal option on the attribute. Whether the cache is per-principal is derived automatically by the SDK from the method signature: if the method takes a Principal parameter, the result is cached per principal and HTTP responses switch to Cache-Control: private with Vary: Authorization.

use golem_rust::agentic::Principal;

// Principal-aware: cached per principal (uses_principal auto-derived = true)
#[read_only]
fn my_visible_items(&self, principal: Principal) -> Vec<Item>;

When the method has no Principal parameter, the result is shared across all callers and HTTP responses are Cache-Control: public (CDN-friendly). The Principal parameter is auto-injected by the runtime — it is not part of the method's input schema and is not passed by callers (see golem-add-http-auth-rust).

What Works in a Read-Only Method

A read-only method must be a pure function of the agent's already-loaded state and the method inputs. The operations in the middle column go through Golem's durability layer and trap with AgentError::ReadOnlyViolation before they run and before anything is persisted. The operations in the right column are not detected — they do not trap, but they still break the cache contract and must be avoided by you.

AllowedNot allowed — traps with ReadOnlyViolationNot allowed — not checked, your responsibility
Reading &self fieldsWriting persistent state (storage, databases, …)Mutating in-memory state
Computation over inputsOutgoing HTTP requestsReading the clock / current time
Returning derived valuesRPC calls to other agentsRandomness
Reading environment variables
Remote / blob reads

Common Pitfalls

  • Mutating state, reading a clock, randomness, or env in a read-only method is NOT detected. These do not trap — but they either mutate state that should be immutable here or make the result non-deterministic, which corrupts the cache. The runtime cannot catch them; keeping the method pure is your responsibility. If you need any of them, use a regular (non-read-only) method instead.
  • Writes to persistent state, outgoing HTTP, and RPC do trap. Those go through the durability layer and raise ReadOnlyViolation before running.
  • A read-only method should take &self, not &mut self. Mutating &mut self fields does not trap (it is a plain in-memory write, not a host call), so nothing stops you at runtime — take &self to make the read-only intent explicit.
  • Read-only on an ephemeral agent fails to compile — ephemeral agents have no shared state to read, so the marker has no effect. Remove #[read_only] or change the agent to durable (see golem-stateless-agent-rust).
  • no_cache does not relax the contract — it only disables caching; writes to persistent state / HTTP / RPC still trap, and the same purity rules apply.

Key Points

  • The marker is per-method; an agent can mix read-only and regular methods freely.
  • cache_policy is only expressible inside #[read_only] — non-read-only methods cannot carry one.
  • Read-only methods are the natural fit for HTTP GET/HEAD endpoints (load golem-add-http-endpoint-rust).
  • A read-only method cannot call another agent via RPC; do read-only RPC fan-out from a regular method instead (see golem-call-another-agent-rust).

Signals

GitHub stars
2k
Forks
212
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
golem-mark-read-only-rust
Source
github.com/golemcloud/golem