From 68b838df818117a87493a7acce70cdad847e975b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 17:50:05 +0100 Subject: [PATCH 1/5] docs: orthogonal persistence concept page --- docs/concepts/orthogonal-persistence.md | 149 ++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 11 deletions(-) diff --git a/docs/concepts/orthogonal-persistence.md b/docs/concepts/orthogonal-persistence.md index 7eee9bef..b1be8c51 100644 --- a/docs/concepts/orthogonal-persistence.md +++ b/docs/concepts/orthogonal-persistence.md @@ -3,19 +3,146 @@ title: "Orthogonal Persistence" description: "How canister memory survives across executions and upgrades without databases" sidebar: order: 5 -icskills: [] +icskills: [stable-memory] --- -TODO: Write content for this page. +On traditional backends, application state lives in memory only while the process runs. To persist data across restarts, you need a database -- PostgreSQL, Redis, SQLite, or a file system. The application logic and the storage layer are separate concerns that developers must wire together. - -Explain orthogonal persistence on ICP. Cover how canister memory (heap) persists between calls, stable memory as the upgrade-safe storage layer, heap persistence in Motoko (automatic with enhanced orthogonal persistence), stable structures in Rust (StableBTreeMap, etc.), and the trade-offs between heap and stable memory. Compare with traditional database-backed backends. +On the Internet Computer, persistence is built into the execution model. A canister's memory persists between calls automatically -- no database, no file system, no explicit save/load. You declare a variable, assign it a value, and that value is still there the next time the canister executes. This property is called **orthogonal persistence**: persistence is orthogonal to (independent of) the programming model. - -- Portal: persistence sections (scattered) -- Learn Hub: https://learn.internetcomputer.org (orthogonal persistence) +The practical effect is that the canister IS the database. There is no separate storage tier to configure, query, or maintain. - -- guides/backends/data-persistence -- practical implementation -- languages/rust/stable-structures -- Rust-specific patterns -- guides/canister-management/lifecycle -- persistence across upgrades +## Two memory regions + +Every canister has two distinct memory regions, each with different characteristics: + +### Heap (Wasm linear) memory + +This is regular program memory -- the space where variables, data structures, and the call stack live during execution. It maps to the Wasm linear memory of the canister module. + +- **Size limit:** 4 GB for wasm32 canisters, 6 GB for wasm64 +- **Performance:** Fast, native Wasm memory access +- **Upgrade behavior:** Historically wiped on canister upgrade (Rust); automatically preserved in Motoko with `persistent actor` + +### Stable memory + +A separate, dedicated memory region provided by the Internet Computer runtime. Its sole purpose is to survive canister upgrades. + +- **Size limit:** Hundreds of GB (bounded by the subnet storage limit, approximately 500 GB) +- **Performance:** Slower than heap memory -- each access goes through system API calls rather than direct Wasm memory operations +- **Upgrade behavior:** Always survives upgrades + +The distinction between these two regions is the foundation of all persistence strategies on ICP. + +## How persistence differs by language + +The two mainstream canister languages -- Motoko and Rust -- take fundamentally different approaches to persistence. + +### Motoko: automatic persistence + +With `persistent actor` and `mo:core` 2.0, Motoko delivers true orthogonal persistence. All `let` and `var` declarations inside the actor body are automatically persisted across upgrades. No explicit stable memory management is needed. + +```motoko +import Map "mo:core/Map"; +import Nat "mo:core/Nat"; + +persistent actor { + let users = Map.empty(); + var userCount : Nat = 0; + + // This resets to 0 on every upgrade + transient var requestCount : Nat = 0; + + public func addUser(name : Text) : async Nat { + let id = userCount; + Map.add(users, Nat.compare, id, name); + userCount += 1; + requestCount += 1; + id + }; +} +``` + +Key properties of Motoko persistent actors: + +- **`let` and `var`** declarations persist across upgrades automatically +- **`transient var`** marks data that should reset to its initial value on upgrade (caches, request counters, temporary state) +- **No `stable` keyword needed** -- it is redundant in persistent actors and produces compiler warnings +- **No `pre_upgrade`/`post_upgrade` hooks needed** -- the runtime handles serialization transparently +- **Schema rule:** never change a field's type between upgrades (for example, `Nat` to `Int` will trap and data is unrecoverable). Only add new optional fields. + +This is orthogonal persistence in its purest form -- developers do not think about persistence at all. They write normal code and data survives. + +### Rust: explicit stable structures + +Rust canisters take an explicit approach using the `ic-stable-structures` crate. Data structures are backed directly by stable memory, which means they survive upgrades without any serialization step. + +```rust +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + DefaultMemoryImpl, StableBTreeMap, +}; +use std::cell::RefCell; + +type Memory = VirtualMemory; + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + // This data lives in stable memory -- survives upgrades + static USERS: RefCell, Memory>> = + RefCell::new(StableBTreeMap::init( + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))) + )); +} +``` + +Key properties of Rust stable structures: + +- **`MemoryManager`** partitions stable memory into virtual memories, each assigned a unique `MemoryId` +- **`StableBTreeMap`**, **`StableCell`**, and **`StableLog`** are the primary data structures, each backed by a virtual memory region +- **`#[init]` and `#[post_upgrade]`** handlers must be defined. Stable structures auto-restore, so `post_upgrade` only needs to reinitialize transient state (timers, caches) +- **No `pre_upgrade` serialization needed** -- data is already in stable memory + +For full implementation patterns, see the [Rust stable structures](../languages/rust/stable-structures.md) guide. + +## The dangerous pattern: heap serialization + +Before stable structures existed, the standard approach in Rust was to store data in heap memory (`thread_local! { RefCell> }`) and serialize it to stable memory in `pre_upgrade`, then deserialize it back in `post_upgrade`. + +This pattern has a critical failure mode: `pre_upgrade` runs with a fixed instruction limit. If the dataset grows large enough, serialization exceeds the limit, the hook traps, and the canister is **bricked** -- the upgrade fails and the data cannot be recovered. + +Stable structures avoid this entirely by writing directly to stable memory during normal operation. There is nothing to serialize at upgrade time. + +## Heap vs. stable memory: trade-offs + +| | Heap memory | Stable memory | +|---|---|---| +| **Size limit** | 4 GB (wasm32) / 6 GB (wasm64) | Hundreds of GB | +| **Access speed** | Fast (native Wasm) | Slower (system API calls) | +| **Upgrade safety** | Automatic in Motoko `persistent actor`; wiped in Rust | Always survives upgrades | +| **API** | Native language constructs | `StableBTreeMap` etc. (Rust); automatic (Motoko) | +| **Use case** | Caches, temporary computation | All persistent application data | + +In Motoko with `persistent actor`, this trade-off is largely invisible -- the runtime manages the mapping between heap and stable memory during upgrades. In Rust, developers choose explicitly: heap data (fast but ephemeral) or stable structures (slightly slower but durable). + +## Comparison with traditional backends + +| Concern | Traditional backend | ICP canister | +|---|---|---| +| **State persistence** | External database (PostgreSQL, Redis) | Built into the runtime | +| **Configuration** | Connection strings, schemas, migrations | None (declare variables) | +| **Deployment** | App server + database server | Single canister | +| **Upgrade safety** | Database persists independently of app | Stable memory persists across upgrades | +| **Scaling storage** | Provision database storage separately | Stable memory grows with usage (up to subnet limit) | + +The mental model shift: instead of "my app talks to a database," think "my app IS the database." Canister state is the program's state, and the Internet Computer ensures it persists. + +## Next steps + +- [Data persistence guide](../guides/backends/data-persistence.md) -- practical implementation patterns for both languages +- [Rust stable structures](../languages/rust/stable-structures.md) -- detailed Rust patterns with `StableBTreeMap`, `StableCell`, and `StableLog` +- [Canister lifecycle](../guides/canister-management/lifecycle.md) -- how upgrades, reinstalls, and other lifecycle events interact with persistence + + From 611fbe058fe143e823e9f297e75bc4d22ad2a303 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 18:00:41 +0100 Subject: [PATCH 2/5] fix: add Storable mention and improve Rust stable structures description --- docs/concepts/orthogonal-persistence.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/concepts/orthogonal-persistence.md b/docs/concepts/orthogonal-persistence.md index b1be8c51..f2bcf0e1 100644 --- a/docs/concepts/orthogonal-persistence.md +++ b/docs/concepts/orthogonal-persistence.md @@ -102,10 +102,11 @@ Key properties of Rust stable structures: - **`MemoryManager`** partitions stable memory into virtual memories, each assigned a unique `MemoryId` - **`StableBTreeMap`**, **`StableCell`**, and **`StableLog`** are the primary data structures, each backed by a virtual memory region +- **Custom types need `Storable`** -- keys require `Storable + Ord`, values require `Storable`. Primitive types (`u64`, `String`, `Vec`) implement it automatically - **`#[init]` and `#[post_upgrade]`** handlers must be defined. Stable structures auto-restore, so `post_upgrade` only needs to reinitialize transient state (timers, caches) - **No `pre_upgrade` serialization needed** -- data is already in stable memory -For full implementation patterns, see the [Rust stable structures](../languages/rust/stable-structures.md) guide. +For complete implementation patterns including `Storable` implementations for custom types, see the [Rust stable structures](../languages/rust/stable-structures.md) guide. ## The dangerous pattern: heap serialization From c43703482a036333946f7eb6c22aa5f4ef7eb76b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 10:50:29 +0100 Subject: [PATCH 3/5] docs: address PR feedback for orthogonal persistence page - Fix memory sizes to use GiB consistently (500 GiB, 4 GiB, 6 GiB) - Add subnet shared storage budget note with link to subnet selection - Soften "bricked" to mention skip_pre_upgrade recovery flag - Change #[init]/#[post_upgrade] from "must" to "should" be defined - Remove "historically" from Rust heap wipe description - Make trade-offs table language-aware (Motoko uses heap for all data) - Add shared storage budget bullet to network-overview subnets section --- docs/concepts/network-overview.md | 1 + docs/concepts/orthogonal-persistence.md | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/concepts/network-overview.md b/docs/concepts/network-overview.md index 7c2ea010..97b86f83 100644 --- a/docs/concepts/network-overview.md +++ b/docs/concepts/network-overview.md @@ -20,6 +20,7 @@ When you deploy a canister, it lands on one subnet and is replicated across ever - **Cross-subnet calls.** Canisters on different subnets can call each other through the network's messaging layer. These calls are slightly slower than calls within the same subnet (they require an extra consensus round), but they work transparently — you don't need to know which subnet a canister lives on. - **Subnet size and cost.** Subnets typically range from 13 to 40 nodes. Larger subnets provide stronger security guarantees (more nodes must collude to compromise state) but cost more cycles to run on. Most application canisters run on 13-node subnets. - **Finality.** ICP achieves finality in 1–2 seconds. Once your update call returns, the state change is committed and replicated — there are no probabilistic confirmations or reorgs. +- **Shared storage budget.** All canisters on a subnet share a common storage budget. Each canister can use up to 500 GiB of stable memory, but the total available depends on the subnet's current utilization. Storage-heavy applications should consider subnet selection. - **Geographic distribution.** Nodes within a subnet are distributed across data centers, operators, and jurisdictions to maximize decentralization. Localized subnets also exist for applications with data residency requirements. For details on subnet types and how to choose one, see [Subnet types](../reference/subnet-types.md) and [Subnet selection](../guides/canister-management/subnet-selection.md). diff --git a/docs/concepts/orthogonal-persistence.md b/docs/concepts/orthogonal-persistence.md index f2bcf0e1..219ae569 100644 --- a/docs/concepts/orthogonal-persistence.md +++ b/docs/concepts/orthogonal-persistence.md @@ -20,15 +20,15 @@ Every canister has two distinct memory regions, each with different characterist This is regular program memory -- the space where variables, data structures, and the call stack live during execution. It maps to the Wasm linear memory of the canister module. -- **Size limit:** 4 GB for wasm32 canisters, 6 GB for wasm64 +- **Size limit:** 4 GiB for wasm32 canisters, 6 GiB for wasm64 - **Performance:** Fast, native Wasm memory access -- **Upgrade behavior:** Historically wiped on canister upgrade (Rust); automatically preserved in Motoko with `persistent actor` +- **Upgrade behavior:** Wiped on canister upgrade (Rust) -- use stable structures to persist data; automatically preserved in Motoko with `persistent actor` ### Stable memory A separate, dedicated memory region provided by the Internet Computer runtime. Its sole purpose is to survive canister upgrades. -- **Size limit:** Hundreds of GB (bounded by the subnet storage limit, approximately 500 GB) +- **Size limit:** Up to 500 GiB per canister. The actual available capacity also depends on the subnet's total storage usage, since all canisters on a subnet share a common storage budget. For storage-heavy applications, consider [subnet selection](../guides/canister-management/subnet-selection.md). - **Performance:** Slower than heap memory -- each access goes through system API calls rather than direct Wasm memory operations - **Upgrade behavior:** Always survives upgrades @@ -103,7 +103,7 @@ Key properties of Rust stable structures: - **`MemoryManager`** partitions stable memory into virtual memories, each assigned a unique `MemoryId` - **`StableBTreeMap`**, **`StableCell`**, and **`StableLog`** are the primary data structures, each backed by a virtual memory region - **Custom types need `Storable`** -- keys require `Storable + Ord`, values require `Storable`. Primitive types (`u64`, `String`, `Vec`) implement it automatically -- **`#[init]` and `#[post_upgrade]`** handlers must be defined. Stable structures auto-restore, so `post_upgrade` only needs to reinitialize transient state (timers, caches) +- **`#[init]` and `#[post_upgrade]`** handlers should be defined to reinitialize transient state (timers, caches). Stable structures auto-restore without these hooks, but omitting them may silently leave transient state uninitialized - **No `pre_upgrade` serialization needed** -- data is already in stable memory For complete implementation patterns including `Storable` implementations for custom types, see the [Rust stable structures](../languages/rust/stable-structures.md) guide. @@ -112,7 +112,7 @@ For complete implementation patterns including `Storable` implementations for cu Before stable structures existed, the standard approach in Rust was to store data in heap memory (`thread_local! { RefCell> }`) and serialize it to stable memory in `pre_upgrade`, then deserialize it back in `post_upgrade`. -This pattern has a critical failure mode: `pre_upgrade` runs with a fixed instruction limit. If the dataset grows large enough, serialization exceeds the limit, the hook traps, and the canister is **bricked** -- the upgrade fails and the data cannot be recovered. +This pattern has a critical failure mode: `pre_upgrade` runs with a fixed instruction limit. If the dataset grows large enough, serialization exceeds the limit and the hook traps. The upgrade fails, and recovery requires the `skip_pre_upgrade` flag, which bypasses the failing hook but may result in data loss. Stable structures avoid this entirely by writing directly to stable memory during normal operation. There is nothing to serialize at upgrade time. @@ -120,11 +120,11 @@ Stable structures avoid this entirely by writing directly to stable memory durin | | Heap memory | Stable memory | |---|---|---| -| **Size limit** | 4 GB (wasm32) / 6 GB (wasm64) | Hundreds of GB | +| **Size limit** | 4 GiB (wasm32) / 6 GiB (wasm64) | Up to 500 GiB | | **Access speed** | Fast (native Wasm) | Slower (system API calls) | | **Upgrade safety** | Automatic in Motoko `persistent actor`; wiped in Rust | Always survives upgrades | | **API** | Native language constructs | `StableBTreeMap` etc. (Rust); automatic (Motoko) | -| **Use case** | Caches, temporary computation | All persistent application data | +| **Use case** | All data in Motoko `persistent actor`; caches and temporary computation in Rust | All persistent application data (Rust) | In Motoko with `persistent actor`, this trade-off is largely invisible -- the runtime manages the mapping between heap and stable memory during upgrades. In Rust, developers choose explicitly: heap data (fast but ephemeral) or stable structures (slightly slower but durable). From 2a124b35d289c81d6d2fe08ae8f7cdb83f3a4804 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 11:04:28 +0100 Subject: [PATCH 4/5] docs: restructure orthogonal persistence as pure Diataxis concept page - Remove Motoko and Rust code examples (belong in data-persistence guide) - Clarify that orthogonal persistence is Motoko-exclusive; Rust uses explicit stable structures (not orthogonal) - Replace implementation details with conceptual descriptions + links to the data-persistence how-to guide - Add "Further reading" section with Medium articles and YouTube short - Update data-persistence stub brief to cover content moved from here - Add subnet shared storage budget to network-overview --- docs/concepts/orthogonal-persistence.md | 78 +++++------------------- docs/guides/backends/data-persistence.md | 2 +- 2 files changed, 17 insertions(+), 63 deletions(-) diff --git a/docs/concepts/orthogonal-persistence.md b/docs/concepts/orthogonal-persistence.md index 219ae569..3fc40831 100644 --- a/docs/concepts/orthogonal-persistence.md +++ b/docs/concepts/orthogonal-persistence.md @@ -38,83 +38,31 @@ The distinction between these two regions is the foundation of all persistence s The two mainstream canister languages -- Motoko and Rust -- take fundamentally different approaches to persistence. -### Motoko: automatic persistence +### Motoko: true orthogonal persistence -With `persistent actor` and `mo:core` 2.0, Motoko delivers true orthogonal persistence. All `let` and `var` declarations inside the actor body are automatically persisted across upgrades. No explicit stable memory management is needed. +Motoko is the only ICP language that delivers true orthogonal persistence. With `persistent actor`, all variable declarations inside the actor body are automatically persisted across upgrades. Developers do not think about persistence at all -- they write normal code and data survives. -```motoko -import Map "mo:core/Map"; -import Nat "mo:core/Nat"; +The runtime transparently manages the mapping between the program's heap and stable memory during upgrades. Fields marked `transient var` reset to their initial value on upgrade, giving developers explicit control over what is ephemeral (caches, counters) versus durable. -persistent actor { - let users = Map.empty(); - var userCount : Nat = 0; +This is orthogonal persistence in its purest form: persistence is completely invisible to the programming model. - // This resets to 0 on every upgrade - transient var requestCount : Nat = 0; - - public func addUser(name : Text) : async Nat { - let id = userCount; - Map.add(users, Nat.compare, id, name); - userCount += 1; - requestCount += 1; - id - }; -} -``` - -Key properties of Motoko persistent actors: - -- **`let` and `var`** declarations persist across upgrades automatically -- **`transient var`** marks data that should reset to its initial value on upgrade (caches, request counters, temporary state) -- **No `stable` keyword needed** -- it is redundant in persistent actors and produces compiler warnings -- **No `pre_upgrade`/`post_upgrade` hooks needed** -- the runtime handles serialization transparently -- **Schema rule:** never change a field's type between upgrades (for example, `Nat` to `Int` will trap and data is unrecoverable). Only add new optional fields. - -This is orthogonal persistence in its purest form -- developers do not think about persistence at all. They write normal code and data survives. +For implementation details and code examples, see the [Data persistence guide](../guides/backends/data-persistence.md). ### Rust: explicit stable structures -Rust canisters take an explicit approach using the `ic-stable-structures` crate. Data structures are backed directly by stable memory, which means they survive upgrades without any serialization step. - -```rust -use ic_stable_structures::{ - memory_manager::{MemoryId, MemoryManager, VirtualMemory}, - DefaultMemoryImpl, StableBTreeMap, -}; -use std::cell::RefCell; - -type Memory = VirtualMemory; +Rust canisters take an explicit approach. The `ic-stable-structures` crate provides data structures (`StableBTreeMap`, `StableCell`, `StableLog`) that are backed directly by stable memory. Data written to these structures survives upgrades without any serialization step. -thread_local! { - static MEMORY_MANAGER: RefCell> = - RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); +This is not orthogonal persistence -- developers must consciously choose which data structures to use and how to partition stable memory. The tradeoff is full control: Rust developers decide exactly what persists, how it's stored, and how memory is allocated. - // This data lives in stable memory -- survives upgrades - static USERS: RefCell, Memory>> = - RefCell::new(StableBTreeMap::init( - MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))) - )); -} -``` - -Key properties of Rust stable structures: - -- **`MemoryManager`** partitions stable memory into virtual memories, each assigned a unique `MemoryId` -- **`StableBTreeMap`**, **`StableCell`**, and **`StableLog`** are the primary data structures, each backed by a virtual memory region -- **Custom types need `Storable`** -- keys require `Storable + Ord`, values require `Storable`. Primitive types (`u64`, `String`, `Vec`) implement it automatically -- **`#[init]` and `#[post_upgrade]`** handlers should be defined to reinitialize transient state (timers, caches). Stable structures auto-restore without these hooks, but omitting them may silently leave transient state uninitialized -- **No `pre_upgrade` serialization needed** -- data is already in stable memory - -For complete implementation patterns including `Storable` implementations for custom types, see the [Rust stable structures](../languages/rust/stable-structures.md) guide. +For implementation details and code examples, see the [Data persistence guide](../guides/backends/data-persistence.md). ## The dangerous pattern: heap serialization -Before stable structures existed, the standard approach in Rust was to store data in heap memory (`thread_local! { RefCell> }`) and serialize it to stable memory in `pre_upgrade`, then deserialize it back in `post_upgrade`. +Before stable structures existed, the standard approach in Rust was to store data in heap memory and serialize it to stable memory in `pre_upgrade`, then deserialize it back in `post_upgrade`. This pattern has a critical failure mode: `pre_upgrade` runs with a fixed instruction limit. If the dataset grows large enough, serialization exceeds the limit and the hook traps. The upgrade fails, and recovery requires the `skip_pre_upgrade` flag, which bypasses the failing hook but may result in data loss. -Stable structures avoid this entirely by writing directly to stable memory during normal operation. There is nothing to serialize at upgrade time. +Stable structures avoid this entirely by writing directly to stable memory during normal operation. There is nothing to serialize at upgrade time. New Rust canisters should always use stable structures rather than heap serialization. ## Heap vs. stable memory: trade-offs @@ -140,6 +88,12 @@ In Motoko with `persistent actor`, this trade-off is largely invisible -- the ru The mental model shift: instead of "my app talks to a database," think "my app IS the database." Canister state is the program's state, and the Internet Computer ensures it persists. +## Further reading + +- [IC Internals: Orthogonal Persistence](https://medium.com/dfinity/ic-internals-orthogonal-persistence-9e0c094aac1a) -- deep dive into how orthogonal persistence works at the protocol level +- [A Journey into Stellarator (Part 2)](https://medium.com/dfinity/a-journey-into-stellarator-part-2-d4a83c631748) -- the Stellarator engine that powers Motoko's persistent actors +- [Orthogonal Persistence in 60 Seconds](https://www.youtube.com/shorts/g3sC2wjLzew) -- quick visual explainer + ## Next steps - [Data persistence guide](../guides/backends/data-persistence.md) -- practical implementation patterns for both languages diff --git a/docs/guides/backends/data-persistence.md b/docs/guides/backends/data-persistence.md index cd5bc717..968196d2 100644 --- a/docs/guides/backends/data-persistence.md +++ b/docs/guides/backends/data-persistence.md @@ -9,7 +9,7 @@ icskills: [stable-memory] TODO: Write content for this page. -Guide developers through storing data in canisters. Cover stable structures (StableBTreeMap in Rust), persistent actors (Motoko), MemoryManager for multiple data structures, and pre/post-upgrade hooks. Include idempotency patterns for safe data mutation. Show code examples for both Rust and Motoko. +Guide developers through storing data in canisters. This is the how-to companion to concepts/orthogonal-persistence (which explains what and why; this page shows how). Cover: Motoko persistent actors (persistent actor, transient var, let/var persistence, schema evolution rules), Rust stable structures (StableBTreeMap, StableCell, StableLog, MemoryManager, MemoryId partitioning, Storable trait implementations for custom types, #[init]/#[post_upgrade] hook patterns), and the dangerous pre_upgrade heap serialization anti-pattern. Include idempotency patterns for safe data mutation. Show complete code examples for both Rust and Motoko. - Portal: building-apps/canister-management/storage.mdx, best-practices/storage.mdx, best-practices/idempotency.mdx From c05d0a5a8c66b54e71575c35d7066cafddeffe49 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 17 Mar 2026 13:33:37 +0100 Subject: [PATCH 5/5] docs: clarify intro and align heap memory limits across site - Intro now distinguishes Motoko (transparent) vs Rust (explicit) persistence - Comparison table uses precise "500 GiB per canister, subject to subnet storage budget" instead of vague "up to subnet limit" - Align heap memory to "4 GiB (wasm32) / 6 GiB (wasm64)" in canisters.md and cycles-costs.md to match orthogonal-persistence.md and icskills --- docs/concepts/canisters.md | 2 +- docs/concepts/orthogonal-persistence.md | 6 +++--- docs/reference/cycles-costs.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/concepts/canisters.md b/docs/concepts/canisters.md index 087230b9..8064d7e6 100644 --- a/docs/concepts/canisters.md +++ b/docs/concepts/canisters.md @@ -69,7 +69,7 @@ Each canister has two storage regions: | Region | Max size | Persisted across upgrades | Access | |--------|----------|--------------------------|--------| -| **Heap (Wasm) memory** | 6 GiB | No (cleared on upgrade, unless using Motoko's orthogonal persistence) | Standard Wasm memory instructions | +| **Heap (Wasm) memory** | 4 GiB (wasm32) / 6 GiB (wasm64) | No (cleared on upgrade, unless using Motoko's orthogonal persistence) | Standard Wasm memory instructions | | **Stable memory** | 500 GiB | Yes | System API calls | **Heap memory** is standard Wasm linear memory. It holds your program's heap-allocated data — variables, data structures, and anything your code allocates at runtime. Both 32-bit and 64-bit Wasm memory are supported. Heap memory is cleared when you upgrade the canister's Wasm module. diff --git a/docs/concepts/orthogonal-persistence.md b/docs/concepts/orthogonal-persistence.md index 3fc40831..563178ab 100644 --- a/docs/concepts/orthogonal-persistence.md +++ b/docs/concepts/orthogonal-persistence.md @@ -8,9 +8,9 @@ icskills: [stable-memory] On traditional backends, application state lives in memory only while the process runs. To persist data across restarts, you need a database -- PostgreSQL, Redis, SQLite, or a file system. The application logic and the storage layer are separate concerns that developers must wire together. -On the Internet Computer, persistence is built into the execution model. A canister's memory persists between calls automatically -- no database, no file system, no explicit save/load. You declare a variable, assign it a value, and that value is still there the next time the canister executes. This property is called **orthogonal persistence**: persistence is orthogonal to (independent of) the programming model. +On the Internet Computer, persistence is built into the execution model. A canister's memory persists between calls automatically -- no database and no file system. In Motoko, this is fully transparent: you declare a variable, assign it a value, and that value is still there the next time the canister executes -- no explicit save or load. In Rust, you choose persistent data structures that write directly to stable memory, giving you full control over what survives upgrades. Either way, the canister IS its own storage. This property is called **orthogonal persistence**: persistence is orthogonal to (independent of) the programming model. -The practical effect is that the canister IS the database. There is no separate storage tier to configure, query, or maintain. +There is no separate storage tier to configure, query, or maintain. ## Two memory regions @@ -84,7 +84,7 @@ In Motoko with `persistent actor`, this trade-off is largely invisible -- the ru | **Configuration** | Connection strings, schemas, migrations | None (declare variables) | | **Deployment** | App server + database server | Single canister | | **Upgrade safety** | Database persists independently of app | Stable memory persists across upgrades | -| **Scaling storage** | Provision database storage separately | Stable memory grows with usage (up to subnet limit) | +| **Scaling storage** | Provision database storage separately | Stable memory grows with usage (up to 500 GiB per canister, subject to subnet storage budget) | The mental model shift: instead of "my app talks to a database," think "my app IS the database." Canister state is the program's state, and the Internet Computer ensures it persists. diff --git a/docs/reference/cycles-costs.md b/docs/reference/cycles-costs.md index db7fc83b..aa4203d5 100644 --- a/docs/reference/cycles-costs.md +++ b/docs/reference/cycles-costs.md @@ -115,7 +115,7 @@ Reserved cycles are non-transferable. Controllers can disable reservation by set | Max same-subnet inter-canister request payload | 10 MiB | | Max response size (replicated execution) | 2 MiB | | Max response size (query) | 3 MiB | -| Wasm heap memory per canister | 4 GiB | +| Wasm heap memory per canister | 4 GiB (wasm32) / 6 GiB (wasm64) | | Wasm stable memory per canister | 500 GiB | | Subnet capacity (total memory) | 2 TiB | | Wasm module total size | 100 MiB |