For AI agents: Documentation index at /llms.txt

Skip to content

Rust CDK

Rust is a natural fit for building canisters on the Internet Computer. Its strong type system, zero-cost abstractions, and memory safety guarantees produce reliable, high-performance Wasm modules. The Rust CDK (Canister Development Kit) provides the libraries and macros that connect your Rust code to the ICP system API.

The Rust CDK is split into focused crates that you pull in as needed:

CratePurpose
ic-cdkCore library: system API bindings, inter-canister calls, canister state
ic-cdk-macrosProcedural macros that register Rust functions as canister entry points (re-exported by ic-cdk)
ic-cdk-timersOne-shot and periodic timer scheduling

You will also commonly use these companion crates:

CratePurpose
candidCandid serialization: types, encoding, decoding
ic-stable-structuresPersistent data structures that survive canister upgrades (see Stable Structures)

A minimal counter canister with a query and an update method:

use std::cell::RefCell;
thread_local! {
static COUNTER: RefCell<u64> = RefCell::new(0);
}
#[ic_cdk::query]
fn get() -> u64 {
COUNTER.with(|c| *c.borrow())
}
#[ic_cdk::update]
fn increment() -> u64 {
COUNTER.with(|c| {
let mut val = c.borrow_mut();
*val += 1;
*val
})
}
ic_cdk::export_candid!();

The #[query] and #[update] macros register Rust functions as canister endpoints. export_candid!() generates the Candid interface file so callers and tools can discover your canister’s API automatically.

Install the Rust toolchain and the Wasm compilation target:

Terminal window
rustup target add wasm32-unknown-unknown

You also need candid-extractor, which the Rust recipe uses to auto-generate Candid interface files:

Terminal window
cargo install candid-extractor

Create a new project using the Rust template:

Terminal window
icp new my_project --subfolder rust

This generates an icp.yaml with a Rust canister recipe and a Cargo workspace. The key files are:

icp.yaml: declares the canister and its build recipe:

canisters:
- name: backend
recipe:
type: "@dfinity/rust@<version>"
configuration:
package: backend
shrink: true

Cargo.toml: must set the crate type to cdylib so the compiler produces a Wasm module:

[lib]
crate-type = ["cdylib"]
[dependencies]
ic-cdk = "0.19"
candid = "0.10"

Start a local network and deploy:

Terminal window
icp network start -d
icp deploy

See the Quickstart for a full walkthrough.

Every canister entry point is a plain Rust function annotated with a procedural macro. The macros handle Candid serialization and hook into the ICP system API.

MacroDescription
#[query]Registers a read-only query method. Queries are fast (no consensus) but cannot modify state.
#[update]Registers a state-modifying update method. Updates go through consensus.

Both macros accept an optional name argument to set the Candid method name if it should differ from the Rust function name:

#[ic_cdk::query(name = "greet")]
fn greet_user(name: String) -> String {
format!("Hello, {name}!")
}
MacroWhen it runs
#[init]Once, when the canister is first created. Use it to initialize state.
#[pre_upgrade]Before a canister upgrade. Save any state that must survive the upgrade.
#[post_upgrade]After a canister upgrade. Restore state saved in pre_upgrade.
MacroPurpose
#[heartbeat]Called once per round by the system. Prefer ic-cdk-timers for scheduled work.
#[inspect_message]Runs before update calls. Call ic_cdk::api::accept_message() to allow the call, or trap to reject it.
#[on_low_wasm_memory]Triggered when Wasm memory usage crosses the configured threshold.
export_candid!()Generates the .did Candid interface file from all annotated endpoints. Place this at the module root.

Entry points can be async. The CDK embeds its own async executor, so you can .await inter-canister calls directly:

use candid::Principal;
use ic_cdk::call::Call;
#[ic_cdk::update]
async fn call_other(canister: Principal) -> String {
Call::bounded_wait(canister, "greet")
.with_arg("World")
.await
.expect("call failed")
.candid::<(String,)>()
.expect("decode failed")
.0
}

If you need to run work in the background after replying, use ic_cdk::futures::spawn:

#[ic_cdk::update]
async fn fire_and_forget() {
ic_cdk::futures::spawn(async {
// background work here
});
}

Important: Do not use tokio, async-std, or other async runtimes. They rely on OS threading and I/O primitives that are not available in the Wasm execution environment. Use the CDK’s built-in executor and ic_cdk::futures::spawn instead.

Rust canisters on ICP benefit from orthogonal persistence: heap data is preserved across update calls automatically. However, heap data is lost during canister upgrades unless you explicitly save and restore it.

Two strategies for handling upgrades:

  1. Stable structures: use the ic-stable-structures crate to store data in stable memory, which survives upgrades without any serialization. This is the recommended approach. See Stable Structures.
  2. Pre/post upgrade hooks: serialize heap data in #[pre_upgrade] and deserialize in #[post_upgrade]. Simpler for small state but does not scale well.

For a deeper look at persistence patterns across languages, see the Data persistence guide.

Rust canisters compile to wasm32-unknown-unknown. Most pure-computation crates work out of the box, but anything that touches OS-level I/O, threading, or time does not. Here are the common issues and their solutions:

What you needStandard RustICP equivalent
Threads / parallelismstd::thread, rayonNot available. Use ic_cdk::futures::spawn for concurrent async tasks.
Sleep / delaysstd::thread::sleepUse ic-cdk-timers to schedule future execution.
Current timestd::time::Instantic_cdk::api::time() returns nanoseconds since the epoch.
Environment variablesstd::env::varNot available at runtime. Use env!() or option_env!() to embed values at compile time.
Random numbersrand, getrandomUse ic_cdk::management_canister::raw_rand() for verifiable randomness, or implement getrandom::register_custom_getrandom! for crates that depend on getrandom.
Network I/Oreqwest, hyperUse HTTPS outcalls via the management canister.

Most crates that target wasm32-unknown-unknown for browser use (via wasm-bindgen or wasm-pack) will not work because they depend on JavaScript host bindings that do not exist in the ICP runtime.