What are canisters on the Internet Computer, and how do cycles pay for them? · Jumpstart Blockchain
What are canisters on the Internet Computer, and how do cycles pay for them?
A canister is Wasm code plus heap and stable memory. It pays for compute, messages, storage and HTTPS outcalls in stable-priced cycles made from ICP; below its freezing threshold it freezes, and at zero it's wiped.
On most chains, a smart contract is code plus a little storage, and every user pays gas to call it. On the Internet Computer (ICP), the unit of deployment is a canister: a WebAssembly program with its own memory, which can hold gigabytes of state, serve web pages and make HTTPS requests. And the canister, not the user, pays for everything it does, using cycles.
What a canister is
A canister bundles:
A Wasm module. Written in Motoko, Rust, or other languages that compile to WebAssembly.
Heap (Wasm) memory. The program's normal in-memory state. It is persisted between calls automatically, but by default it is reset when you upgrade the code.
Stable memory. A separate, much larger memory region that survives upgrades. Anything you can't afford to lose on an upgrade belongs here, or in a data structure built on it.
Settings and controllers. Controllers are the principals (identities or other canisters) allowed to install and upgrade code or change settings. A canister with no controllers can never be changed.
A cycles balance. Its fuel tank.
Each canister has a principal ID such as ryjl3-tyaaa-aaaaa-aaaba-cai and exposes a public interface described in Candid, ICP's interface description language.
Subnets
Canisters don't run on every node. The network is split into subnets, each a group of nodes (commonly 13, with larger subnets for high-value applications) running their own consensus and replicating the canisters assigned to them. Canisters on different subnets talk through asynchronous messages. Chain-key cryptography lets anyone verify a response from any subnet against a single network public key, which is also what lets canisters sign transactions for other chains.
Costs scale with subnet size: running on a larger subnet costs proportionally more cycles, because more nodes replicate the work.
The reverse-gas model
Users calling a canister need no tokens and no wallet. The canister's developer or owner keeps it funded, much like paying a cloud bill. That's the reverse-gas model, and it's why ICP apps can feel like ordinary websites.
The flip side is that you are paying for anyone who calls your canister, so anything expensive must be rate-limited or restricted to authenticated users.
Cycles and ICP
Cycles are the resource unit. They are deliberately stable in price: 1 trillion cycles (1T) is minted from ICP worth one XDR (the IMF's Special Drawing Right, roughly one and a third US dollars at recent rates). The conversion runs through the cycles minting canister, which uses the current ICP/XDR rate, so your costs don't swing with the ICP price. Converting burns the ICP; cycles can't be turned back into ICP.
With dfx, the current DFINITY SDK CLI:
dfx identity get-principal
dfx ledger account-id # send ICP here to fund this identity
dfx cycles convert --amount 2 --network ic # burn 2 ICP, credit cycles to your cycles ledger balance
dfx cycles balance --network ic
dfx deploy --network ic # creates canisters and pays for them from that balance
dfx cycles top-up counter 1000000000000 --network ic # add 1T cycles to a canister
DFINITY has also been developing a newer CLI (icp-cli) intended to succeed dfx. Check which one the current docs use; the concepts are the same.
What consumes cycles
Resource
Charged for
Compute
Wasm instructions executed by update calls, timers and heartbeats
Messages
A per-message fee plus per-byte fees for ingress messages and inter-canister calls
Storage
Heap and stable memory, charged per byte per second, even when idle
HTTPS outcalls
A base fee plus per-byte fees for the request and the maximum response size you allow
Threshold signatures
Chain-key ECDSA/Schnorr signing, per signature
Allocations
Reserved compute or memory, if you set them
Canister creation
A one-time creation fee
Two practical notes. Query calls currently aren't charged, though the fee schedule can change through governance. And an HTTPS outcall is executed by every node in the subnet, so set max_response_bytes as small as you can: you pay for the maximum, not what comes back.
Freezing threshold and running out
Storage is charged every second, so a canister's balance drains even when nobody uses it. To avoid sudden death, each canister has a freezing threshold, 30 days by default. When the balance falls below what the canister would need to pay its idle costs for that period, it freezes: it stops executing update calls and sending messages, and its remaining cycles pay for storage.
If it keeps draining to zero, the canister is uninstalled: its code and memory, including stable memory, are deleted. The canister ID and its controllers remain, but the data is gone. There is no grace period beyond the freezing threshold.
Protect production canisters:
dfx canister status counter --network ic # balance, memory size, idle burn per day
dfx canister update-settings counter --freezing-threshold 7776000 --network ic # 90 days
Anyone can send cycles to any canister, so a monitoring job or a top-up service can keep balances healthy. Alert well before the freezing threshold, not at it.
Update calls vs query calls
Update call
Query call
Goes through consensus
Yes
No, answered by one node
Can change state
Yes, changes persist
Changes are discarded
Latency
Roughly one to a few seconds
Milliseconds
Trust
Response is certified by the subnet
Trust the node that answered, unless you use certified data
Use queries for reads that need speed, and return certified variables (data the subnet has signed during an earlier update) when the client needs to verify what a single node told it. Anything that moves value should be an update call.
A small Motoko canister
persistent actor Counter {
var count : Nat = 0;
public func increment() : async Nat {
count += 1;
count
};
public query func get() : async Nat {
count
};
};
In a persistent actor, variables are kept across upgrades by default (use transient for ones that should reset). increment is an update call and costs cycles; get is a query. In Rust, the equivalent heap state in a thread_local! would be wiped on upgrade unless you save it in pre/post-upgrade hooks or keep it in stable structures such as those from the ic-stable-structures crate. That upgrade-time data loss is one of the most common ICP mistakes.
Local deployments use free, fabricated cycles, so measure real costs on mainnet or with the cost estimates in the docs before you promise anyone a budget.
Summary
Concept
Key point
Canister
Wasm code plus heap and stable memory, with controllers
Cycles
Stable-priced fuel; 1T cycles comes from ICP worth 1 XDR
Reverse gas
The canister pays, users don't
Freezing threshold
Stops updates early to protect storage; default 30 days
Zero balance
Canister is uninstalled and its state deleted
Update vs query
Consensus and persistence vs fast, single-node reads