Skip to Content
How-To GuidesRustCustom Snapshots in Rust

Custom Snapshots in Rust

Golem agents can implement custom save_snapshot and load_snapshot functions to support manual (snapshot-based) updates and snapshot-based recovery.

When to Use Snapshotting

Snapshotting solves two distinct problems:

  1. Manual / snapshot-based component updates — required when updating agents between incompatible component versions.
  2. Fast recovery and oplog compaction — for long-running agents whose oplog grows over time (heartbeats, polling loops, recurring tasks, agents with frequent state changes). Without snapshotting, every recovery replays the full oplog from the beginning, which becomes increasingly expensive. With periodic snapshotting (every(N) or periodic(...)), recovery starts from the latest snapshot and replays only the entries after it.

You cannot opt out of oplog writes for a durable agent. If you are worried about oplog volume or replay cost, do not try to skip persistence — enable snapshot-based recovery here instead.

Enabling Snapshotting

Snapshotting must be enabled via the snapshotting attribute on #[agent_definition]. Without it, snapshotting is disabled:

#[agent_definition(mount = "/counters/{name}", snapshotting = "every(1)")] pub trait CounterAgent { fn new(name: String) -> Self; #[endpoint(post = "/increment")] fn increment(&mut self) -> u32; }

Snapshotting Modes

The snapshotting attribute accepts these values:

ModeExampleDescription
"disabled"(default when omitted)No snapshotting
"enabled"snapshotting = "enabled"Enable snapshot support with the server’s default policy. The server default is disabled, so this may have no effect. Use "every(N)" or "periodic(…)" to guarantee snapshotting is active.
"every(N)"snapshotting = "every(1)"Snapshot every N successful function calls (use "every(1)" for every invocation)
"periodic(duration)"snapshotting = "periodic(30s)"Snapshot at most once per time interval (uses humantime durations)

An ephemeral agent takes no snapshots, whatever its snapshotting value. Snapshots are for durable agents only.

#[agent_definition(mount = "/periodic/{name}", snapshotting = "periodic(30s)")] pub trait PeriodicAgent { ... } #[agent_definition(mount = "/batch/{name}", snapshotting = "every(10)")] pub trait BatchAgent { ... }

Automatic Snapshotting (Default)

When snapshotting is enabled and the implementation has no custom save_snapshot/load_snapshot, the SDK provides JSON-based snapshotting — no custom code needed. The agent struct must then implement serde::Serialize and serde::de::DeserializeOwned; otherwise the build fails. The macros generate the snapshot handlers.

use serde::{Serialize, Deserialize}; use golem_rust::{agent_definition, agent_implementation, endpoint}; #[agent_definition(mount = "/counters/{name}", snapshotting = "every(1)")] pub trait CounterAgent { fn new(name: String) -> Self; #[endpoint(post = "/increment")] fn increment(&mut self) -> u32; } #[derive(Serialize, Deserialize)] // This enables automatic snapshotting struct CounterImpl { name: String, count: u32, } #[agent_implementation(mount = "/counters/{name}")] impl CounterAgent for CounterImpl { fn new(name: String) -> Self { Self { name, count: 0 } } #[endpoint(post = "/increment")] fn increment(&mut self) -> u32 { self.count += 1; self.count } // No save_snapshot/load_snapshot needed — serde handles it automatically }

Custom Snapshotting

For custom binary formats, compatibility with non-Rust components, or migration between different state schemas, implement both save_snapshot and load_snapshot on the agent implementation:

use golem_rust::{agent_definition, agent_implementation, endpoint}; #[agent_definition(mount = "/snapshot-counters/{name}", snapshotting = "every(1)")] pub trait CounterWithSnapshotAgent { fn new(name: String) -> Self; #[endpoint(post = "/increment")] fn increment(&mut self) -> u32; } struct CounterImpl { _name: String, count: u32, } #[agent_implementation(mount = "/snapshot-counters/{name}")] impl CounterWithSnapshotAgent for CounterImpl { fn new(name: String) -> Self { Self { _name: name, count: 0, } } #[endpoint(post = "/increment")] fn increment(&mut self) -> u32 { self.count += 1; log::info!("The new value is {}", self.count); self.count } async fn load_snapshot( bytes: Vec<u8>, context: golem_rust::agentic::SnapshotRestoreContext, ) -> Result<Self, String> { let arr: [u8; 4] = bytes .try_into() .map_err(|_| "Expected a 4-byte long snapshot")?; let mut parameters = golem_rust::agentic::DirectAgentInput::new(context.parameters) .map_err(|error| error.to_string())?; let name: String = parameters .take() .map_err(|error| error.to_string())?; parameters.finish().map_err(|error| error.to_string())?; Ok(Self { _name: name, count: u32::from_be_bytes(arr), }) } async fn save_snapshot(&self) -> Result<Vec<u8>, String> { Ok(self.count.to_be_bytes().to_vec()) } }

Rules

  • Both save_snapshot and load_snapshot must be implemented together, or neither. The macro enforces this at compile time.
  • When custom implementations are present, automatic serde-based snapshotting is bypassed.
  • save_snapshot returns Result<Vec<u8>, String> — the bytes are the snapshot payload.
  • load_snapshot is an associated restoration factory. It receives Vec<u8> and SnapshotRestoreContext, then returns a complete Self in Result<Self, String>.
  • Restoration does not call new. Use the context’s parameters, principal, agent_type, and phantom_id fields to reconstruct identity-dependent state. Agent config can be read through Config::<T>::new().get() while restoring.
  • parameters is a SchemaValueTree. Pass it to DirectAgentInput::new, call take::<T>() once for each constructor parameter in declaration order, then call finish() to reject extra values and incomplete wire data. Each method returns a WireError on malformed input.
  • Both methods are async — they can perform asynchronous operations during serialization/deserialization.
  • Returning Err from load_snapshot rejects the incomplete instance. A manual update fails and remains on the previous component version. During automatic recovery, Golem recreates the component without the failed automatic snapshot. It then tries the previous usable automatic snapshot. After that, it uses the authoritative baseline of the last successful update or a full replay. During an automatic update, the update fails, and the agent stays on its source revision: with UPDATE_SNAPSHOT_INCOMPATIBLE when the update selected an automatic snapshot, else with UPDATE_REPLAY_FAILED. Without a pending update, when the snapshot of the last successful update does not load, the start of the agent fails.

Method Signatures

// Save: serialize the agent's current state to bytes async fn save_snapshot(&self) -> Result<Vec<u8>, String> // Load: construct a complete agent from previously saved bytes and restore context async fn load_snapshot( bytes: Vec<u8>, context: SnapshotRestoreContext, ) -> Result<Self, String>

Restoration Is Read-Only

load_snapshot is a specially supported SDK lifecycle operation, not an agent method. Golem runs it in read-only mode and does not write anything it does to the oplog. Decoding, local computation, fresh randomness, config reads, and other permitted reads can be used to build the returned value. Mutating host operations and outgoing HTTP or agent RPC calls are rejected before they take effect.

Do not initialize an ordinary instance and mutate it after loading. Construct and return the complete restored Self; the SDK installs it only after the factory succeeds.

Best Practices

  1. Prefer automatic (serde) snapshotting unless you need a compact binary format or cross-version migration logic.
  2. Keep snapshots small — large snapshots impact recovery and update time.
  3. Version your snapshot format — include a version byte or tag so load_snapshot can handle snapshots from older versions.
  4. Test round-trips — verify that save_snapshot → load_snapshot produces equivalent state without calling new.
  5. Handle migration — when the state schema changes between versions, load_snapshot in the new version should be able to parse snapshots from the old version.

Project Template

A ready-made project with snapshotting can be created using:

golem new --yes --template rust/snapshotting my-project