From 782c7b25c18cebe3f463ddac5843aabbcbb81ab5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 05:25:33 +0200 Subject: [PATCH 1/2] docs(canister-management): write cycles management guide Covers ICP-to-cycles conversion via CMC, monitoring cycle balances, freezing thresholds, programmatic top-ups in Motoko and Rust, and multi-environment deployment with a production readiness checklist. Renames stub from .md to .mdx to support language-synced Motoko/Rust tabs. --- .../canister-management/cycles-management.md | 26 -- .../canister-management/cycles-management.mdx | 431 ++++++++++++++++++ 2 files changed, 431 insertions(+), 26 deletions(-) delete mode 100644 docs/guides/canister-management/cycles-management.md create mode 100644 docs/guides/canister-management/cycles-management.mdx diff --git a/docs/guides/canister-management/cycles-management.md b/docs/guides/canister-management/cycles-management.md deleted file mode 100644 index 52dbb1d7..00000000 --- a/docs/guides/canister-management/cycles-management.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: "Cycles Management" -description: "Acquire cycles, manage canister budgets, and deploy to mainnet" -sidebar: - order: 7 ---- - -TODO: Write content for this page. - - -The essential guide for going to mainnet. Cover ICP-to-cycles conversion via the Cycles Minting Canister (CMC), topping up canisters, setting freezing thresholds, monitoring cycle balance, and multi-environment deployment (local/staging/production). Include a production deployment checklist linking to settings, reproducible builds, and security pages. Answer the key question: "how much will this cost me?" - - -- Portal: building-apps/canister-management/topping-up.mdx, getting-started/tokens-and-cycles.mdx -- icp-cli: guides/deploying-to-mainnet.md, guides/tokens-and-cycles.md, guides/managing-environments.md -- icskills: cycles-management -- Examples: hello_cycles (Motoko) -- Template: proxy (cycle forwarding) -- Learn Hub: [Cycles Ledger](https://learn.internetcomputer.org/hc/en-us/articles/45034096457748) - - -- concepts/reverse-gas-model -- why canisters pay cycles -- guides/canister-management/settings -- freezing threshold, allocation -- reference/cycles-costs -- exact cost tables -- guides/canister-management/lifecycle -- deploy workflow -- icp-cli docs: https://cli.internetcomputer.org/ diff --git a/docs/guides/canister-management/cycles-management.mdx b/docs/guides/canister-management/cycles-management.mdx new file mode 100644 index 00000000..577fd7f1 --- /dev/null +++ b/docs/guides/canister-management/cycles-management.mdx @@ -0,0 +1,431 @@ +--- +title: "Cycles Management" +description: "Acquire cycles, monitor canister balances, set freezing thresholds, and deploy to mainnet." +sidebar: + order: 7 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Canisters on ICP pay for compute and storage using **cycles**. Unlike gas on Ethereum, cycles are paid by the canister — not the caller. This is ICP's [reverse gas model](../../concepts/reverse-gas-model.md): developers fund their own canisters, and users interact for free. + +This guide covers everything you need to manage cycles in production: acquiring them, monitoring balances, setting thresholds, and deploying to mainnet. + +## How cycles work + +Cycles are priced in XDR (Special Drawing Rights), a stable international reserve asset. **1 trillion cycles (1T) costs approximately 1 XDR**, which is roughly 1.3 USD as of 2025. This pricing is enforced by the Cycles Minting Canister (CMC) and updated via NNS governance proposals, so the cycle cost of a given operation stays stable even as ICP's price fluctuates. + +Canisters are charged continuously for: +- **Storage** — bytes held in stable memory and heap +- **Compute** — CPU cycles consumed by update and query calls +- **Messages** — ingress and inter-canister calls +- **Special operations** — HTTPS outcalls, threshold signatures, Bitcoin API calls + +See [Cycles costs reference](../../reference/cycles-costs.md) for exact cost tables by subnet size. + +### Local vs mainnet cycles + +Local development uses fabricated cycles — canisters on a local network start with a large balance and never actually run out. Code that works locally can fail on mainnet if the canister is underfunded. Always test with realistic cycle amounts before deploying. + +## Acquiring cycles + +To run canisters on mainnet you need ICP tokens, which you convert to cycles via the CMC. + +### Step 1: Create a mainnet identity + +```bash +icp identity new mainnet-deployer +icp identity default mainnet-deployer +icp identity principal +# Output: xxxxx-xxxxx-xxxxx-xxxxx-xxx +``` + +Save your seed phrase — it is shown only once. Without it, you permanently lose access to the identity and any funds it controls. + +### Step 2: Get ICP tokens + +Purchase ICP on an exchange. When withdrawing, use your principal as the destination address (or `icp identity account-id` if the exchange requires an account identifier). + +Verify arrival: + +```bash +icp token balance -n ic +``` + +### Step 3: Convert ICP to cycles + +```bash +# Convert 5 ICP to cycles +icp cycles mint --icp 5 -n ic + +# Or request a specific cycle amount (ICP is calculated automatically) +icp cycles mint --cycles 5T -n ic +``` + +Verify your cycles balance: + +```bash +icp cycles balance -n ic +# Output: ~5T cycles +``` + +**Budget guidance:** Plan for 1–2T cycles per canister as a starting balance. A simple backend canister with moderate traffic costs roughly 0.1–0.5T cycles per month, though this varies with storage and call volume. See the [cycles costs reference](../../reference/cycles-costs.md) for per-operation pricing. + +## Checking canister cycle balances + +Only controllers can view a canister's cycle balance via `icp canister status`. + +### Via icp-cli + +```bash +# Check a canister in your project +icp canister status backend -e ic + +# Check any canister by ID +icp canister status ryjl3-tyaaa-aaaaa-aaaba-cai -n ic +``` + +Example output: + +```text +Status: Running +Controllers: xxxxx-xxxxx-xxxxx-xxxxx-xxx +Memory allocation: 0 +Compute allocation: 0 +Freezing threshold: 2_592_000 +Balance: 9_811_813_913_485 Cycles +``` + +The `Balance` line shows the current cycle balance. The `Freezing threshold` shows how many seconds of idle cycles the canister must retain before freezing (see [Freezing threshold](#freezing-threshold) below). + +### Programmatically + +Canisters can check their own balance at runtime: + + + + +```motoko +import Cycles "mo:core/Cycles"; + +persistent actor { + public query func getBalance() : async Nat { + Cycles.balance() + }; +} +``` + + + + +```rust +use ic_cdk::query; +use candid::Nat; + +#[query] +fn get_balance() -> Nat { + Nat::from(ic_cdk::api::canister_cycle_balance()) +} +``` + + + + +## Topping up canisters + +Anyone can top up any canister — you do not need to be its controller. + +```bash +# Top up by canister name (in your project environment) +icp canister top-up backend --amount 1T -e ic + +# Top up by canister ID (no project context required) +icp canister top-up ryjl3-tyaaa-aaaaa-aaaba-cai --amount 1T -n ic +``` + +Amounts use human-readable suffixes: `T` = trillion, `b` = billion, `m` = million, `k` = thousand. + +To convert ICP and top up in sequence: + +```bash +icp cycles mint --icp 1.0 -n ic +icp canister top-up backend --amount 1T -e ic +``` + +### Accepting cycles in your canister + +Canisters can also accept cycles sent with an inter-canister call. This pattern is used for "tip jar" flows and payment routing: + + + + +```motoko +import Cycles "mo:core/Cycles"; +import Runtime "mo:core/Runtime"; + +persistent actor { + public func deposit() : async Nat { + let available = Cycles.available(); + if (available == 0) { + Runtime.trap("No cycles sent with this call") + }; + Cycles.accept(available) + }; +} +``` + + + + +```rust +use ic_cdk::update; +use candid::Nat; + +#[update] +fn deposit() -> Nat { + let available = ic_cdk::api::msg_cycles_available(); + if available == 0 { + ic_cdk::trap("No cycles sent with this call"); + } + let accepted = ic_cdk::api::msg_cycles_accept(available); + Nat::from(accepted) +} +``` + + + + +## Freezing threshold + +The **freezing threshold** is a canister setting that defines how long (in seconds) a canister can survive on its current balance while idle. When the canister's balance would fall below the estimated cost of running for that many seconds, the canister is frozen: it stops processing update calls but still serves query calls. + +The default is **2,592,000 seconds (30 days)**. Increase it for production canisters or those with large stable memory: + +```bash +# Set freezing threshold to 90 days (7,776,000 seconds) +icp canister settings update backend --freezing-threshold 7776000 -e ic + +# Or use icp.yaml to apply it per-environment +``` + +In `icp.yaml`: + +```yaml +environments: + - name: production + network: ic + canisters: [backend] + settings: + backend: + freezing_threshold: 90d +``` + +See [Canister settings](settings.mdx) for all available settings and their syntax. + +**When a canister is frozen:** +- Update calls return an error immediately +- Query calls still succeed (read-only) +- The canister is not deleted yet — top it up to unfreeze + +**When a frozen canister runs out of cycles entirely:** +- The canister is deleted along with all its state +- This is irreversible + +## Setting a freezing threshold programmatically + +You can also read and configure the freezing threshold from canister code: + + + + +```motoko +import Principal "mo:core/Principal"; + +persistent actor Self { + + type CreateCanisterSettings = { + controllers : ?[Principal]; + compute_allocation : ?Nat; + memory_allocation : ?Nat; + freezing_threshold : ?Nat; + }; + + type CanisterId = { canister_id : Principal }; + + let ic = actor ("aaaaa-aa") : actor { + create_canister : shared { settings : ?CreateCanisterSettings } -> async CanisterId; + deposit_cycles : shared { canister_id : Principal } -> async (); + }; + + // Create a new canister with 1T cycles and a 30-day freezing threshold + public func createWithThreshold() : async Principal { + let result = await (with cycles = 1_000_000_000_000) ic.create_canister({ + settings = ?{ + controllers = ?[Principal.fromActor(Self)]; + compute_allocation = null; + memory_allocation = null; + freezing_threshold = ?2_592_000; // 30 days + }; + }); + result.canister_id + }; + + // Top up another canister programmatically + public func topUp(canisterId : Principal, amount : Nat) : async () { + await (with cycles = amount) ic.deposit_cycles({ canister_id = canisterId }); + }; +} +``` + + + + +```rust +use candid::{Nat, Principal}; +use ic_cdk::update; +use ic_cdk::management_canister::{ + create_canister_with_extra_cycles, deposit_cycles, + CreateCanisterArgs, DepositCyclesArgs, CanisterSettings, +}; + +#[update] +async fn create_with_threshold() -> Principal { + let caller_principal = ic_cdk::api::canister_self(); + + let settings = CanisterSettings { + controllers: Some(vec![caller_principal]), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: Some(Nat::from(2_592_000u64)), // 30 days + reserved_cycles_limit: None, + log_visibility: None, + wasm_memory_limit: None, + wasm_memory_threshold: None, + environment_variables: None, + }; + + let result = create_canister_with_extra_cycles( + &CreateCanisterArgs { settings: Some(settings) }, + 1_000_000_000_000u128, // 1T cycles + ) + .await + .expect("Failed to create canister"); + + result.canister_id +} + +#[update] +async fn top_up_canister(canister_id: Principal, amount: u128) { + deposit_cycles(&DepositCyclesArgs { canister_id }, amount) + .await + .expect("Failed to deposit cycles"); +} +``` + + + + +## Multi-environment deployment + +For production, use separate environments for staging and production to avoid accidentally affecting live canisters. Configure environments in `icp.yaml`: + +```yaml +environments: + - name: staging + network: ic + canisters: [frontend, backend] + settings: + backend: + freezing_threshold: 30d + environment_variables: + LOG_LEVEL: "debug" + + - name: production + network: ic + canisters: [frontend, backend] + settings: + backend: + freezing_threshold: 90d + environment_variables: + LOG_LEVEL: "error" +``` + +Deploy to each environment independently: + +```bash +# Deploy to staging first +icp deploy -e staging + +# Verify, then deploy to production +icp deploy -e production +``` + +Each environment maintains separate canister IDs. Mainnet IDs are stored in `.icp/data/mappings/.ids.json` and should be committed to version control. See [Managing environments](https://cli.internetcomputer.org/) for full configuration options. + +## Production deployment checklist + +Before deploying to mainnet, verify each of the following: + +- **Fund canisters** — Top up all canisters with at least 2–5T cycles each before deploying +- **Set a freezing threshold** — Use 90 days (`7776000` seconds) or more for production +- **Add a backup controller** — Without a backup, losing your identity means losing the canister permanently: + ```bash + icp canister settings update backend --add-controller BACKUP_PRINCIPAL -e ic + ``` +- **Verify cycle balance after deploy** — Check immediately after `icp deploy -e ic`: + ```bash + icp canister status backend -e ic + ``` +- **Enable reproducible builds** — See [Reproducible builds](reproducible-builds.md) to ensure your WASM is verifiable +- **Review canister settings** — See [Canister settings](settings.mdx) for memory allocation, compute allocation, and access controls +- **Review security** — See [Canister upgrades security](../security/canister-upgrades.md) for safe upgrade patterns + +## Monitoring cycle balances + +There is no built-in alerting for low balances — monitoring is your responsibility. Options: + +**Manual monitoring** — Check regularly via icp-cli: + +```bash +# Check all canisters in an environment at once +icp canister status -e ic +``` + +**Automated monitoring services** — Third-party services can monitor balances and alert or auto-top-up: +- [CycleOps](https://cycleops.dev) — Onchain monitoring with automated top-ups and email notifications +- [Canistergeek](https://cusyh-iyaaa-aaaah-qcpba-cai.raw.icp0.io/) — Cycles, memory, and log monitoring in one place + +**Automated top-up libraries:** +- Rust: [canfund](https://github.com/dfinity/canfund) — DFINITY-maintained library for automated canister funding +- Motoko: [cycles-manager](https://github.com/CycleOperators/cycles-manager) — Permissioned multi-canister cycles management + +## Common mistakes + +**Sending cycles to the wrong canister** — Cycles transferred to the wrong principal cannot be recovered. Double-check canister IDs before topping up. + +**Using the wrong flag (`-n` vs `-e`)** — Use `-e ic` for canister operations by name; use `-n ic` for token/cycles operations and canister IDs: +```bash +# Correct +icp canister top-up backend --amount 1T -e ic +icp cycles balance -n ic + +# Incorrect (fails — canister name requires -e) +icp canister top-up backend --amount 1T -n ic +``` + +**Forgetting to add a backup controller** — Your identity is the only controller by default. If you lose access to it, the canister cannot be managed, upgraded, or deleted. + +**Confusing local and mainnet cycles** — Local deployments use fabricated cycles and never freeze. Test with realistic amounts on a staging environment before going to production. + +**Using `ExperimentalCycles` in Motoko** — In `mo:core`, the module is `Cycles`, not `ExperimentalCycles`. `import ExperimentalCycles "mo:base/ExperimentalCycles"` will fail with `mo:core`. Use `import Cycles "mo:core/Cycles"`. + +## Next steps + +- [Canister settings](settings.mdx) — Freezing threshold, memory allocation, compute allocation +- [Canister lifecycle](lifecycle.mdx) — Create, install, upgrade, and delete canisters +- [Cycles costs reference](../../reference/cycles-costs.md) — Exact cost tables per operation +- [Reverse gas model](../../concepts/reverse-gas-model.md) — Why canisters pay for execution +- [Reproducible builds](reproducible-builds.md) — Verify your WASM is trustworthy before deploying +- [icp-cli docs](https://cli.internetcomputer.org/) — Full command reference + +{/* Upstream: informed by dfinity/portal — docs/building-apps/canister-management/topping-up.mdx, docs/building-apps/getting-started/tokens-and-cycles.mdx; dfinity/icp-cli — docs/guides/deploying-to-mainnet.md, docs/guides/tokens-and-cycles.md, docs/guides/managing-environments.md; dfinity/icskills — skills/cycles-management/SKILL.md */} From 60562bc9337b8dff393380c799e1a8c218836492 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 13:50:57 +0200 Subject: [PATCH 2/2] docs(cycles-management): address PR #60 feedback - fix .mdx extension in links --- .../guides/canister-management/cycles-management.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guides/canister-management/cycles-management.mdx b/docs/guides/canister-management/cycles-management.mdx index 577fd7f1..ae97e824 100644 --- a/docs/guides/canister-management/cycles-management.mdx +++ b/docs/guides/canister-management/cycles-management.mdx @@ -140,7 +140,7 @@ Anyone can top up any canister — you do not need to be its controller. icp canister top-up backend --amount 1T -e ic # Top up by canister ID (no project context required) -icp canister top-up ryjl3-tyaaa-aaaaa-aaaba-cai --amount 1T -n ic +icp canister top-up --amount 1T ryjl3-tyaaa-aaaaa-aaaba-cai -n ic ``` Amounts use human-readable suffixes: `T` = trillion, `b` = billion, `m` = million, `k` = thousand. @@ -220,7 +220,7 @@ environments: freezing_threshold: 90d ``` -See [Canister settings](settings.mdx) for all available settings and their syntax. +See [Canister settings](settings.md) for all available settings and their syntax. **When a canister is frozen:** - Update calls return an error immediately @@ -231,7 +231,7 @@ See [Canister settings](settings.mdx) for all available settings and their synta - The canister is deleted along with all its state - This is irreversible -## Setting a freezing threshold programmatically +## Creating and funding canisters programmatically You can also read and configure the freezing threshold from canister code: @@ -377,7 +377,7 @@ Before deploying to mainnet, verify each of the following: icp canister status backend -e ic ``` - **Enable reproducible builds** — See [Reproducible builds](reproducible-builds.md) to ensure your WASM is verifiable -- **Review canister settings** — See [Canister settings](settings.mdx) for memory allocation, compute allocation, and access controls +- **Review canister settings** — See [Canister settings](settings.md) for memory allocation, compute allocation, and access controls - **Review security** — See [Canister upgrades security](../security/canister-upgrades.md) for safe upgrade patterns ## Monitoring cycle balances @@ -421,8 +421,8 @@ icp canister top-up backend --amount 1T -n ic ## Next steps -- [Canister settings](settings.mdx) — Freezing threshold, memory allocation, compute allocation -- [Canister lifecycle](lifecycle.mdx) — Create, install, upgrade, and delete canisters +- [Canister settings](settings.md) — Freezing threshold, memory allocation, compute allocation +- [Canister lifecycle](lifecycle.md) — Create, install, upgrade, and delete canisters - [Cycles costs reference](../../reference/cycles-costs.md) — Exact cost tables per operation - [Reverse gas model](../../concepts/reverse-gas-model.md) — Why canisters pay for execution - [Reproducible builds](reproducible-builds.md) — Verify your WASM is trustworthy before deploying