For AI agents: Documentation index at /llms.txt

Skip to content

Canister Logs

Canister logs help you understand what your canister is doing at runtime, including during traps. The Internet Computer captures log output from update calls, timers, heartbeats, and lifecycle hooks: even when the canister traps mid-execution. Logs are retrievable by canister controllers and optionally by other principals.

Both Rust and Motoko support printing messages to the canister log.

Rust: use ic_cdk::println!:

use ic_cdk::{init, update};
#[init]
fn init() {
ic_cdk::println!("Canister initialized");
}
#[update]
fn process(value: u64) -> u64 {
ic_cdk::println!("Processing value: {}", value);
value * 2
}

The ic_cdk::println! macro formats a string and writes it to the canister log on the IC. Outside of Wasm (for example in unit tests), it falls back to std::println!.

Motoko: use Debug.print from mo:core/Debug:

import Debug "mo:core/Debug";
persistent actor {
public func process(value : Nat) : async Nat {
Debug.print("Processing value: " # debug_show(value));
value * 2
};
};

Debug.print writes to the canister log when running on the IC. In other environments such as the Motoko interpreter, it writes to standard output.

Log messages are recorded for:

  • Update calls
  • Timer and heartbeat executions
  • canister_init, canister_pre_upgrade, and canister_post_upgrade hooks
  • Query calls executed in replicated mode (non-replicated queries are not logged)

Log storage is capped at 4096 bytes by default. When the log buffer is full, the oldest entries are purged. You can increase this limit up to 2 MiB (see Log memory limit).

To fetch and display the logs for a canister:

Terminal window
icp canister logs <canister-name> -e local

To follow logs in real time (polls every 2 seconds by default):

Terminal window
icp canister logs <canister-name> -e local --follow

To adjust the polling interval:

Terminal window
icp canister logs <canister-name> -e local --follow --interval 5

To fetch logs on mainnet, use -e ic:

Terminal window
icp canister logs <canister-name> -e ic

You can scope log output to a specific time range or index range.

By timestamp (RFC3339 or nanoseconds since Unix epoch):

Terminal window
icp canister logs <canister-name> -e ic \
--since 2024-01-01T00:00:00Z \
--until 2024-01-02T00:00:00Z

By log entry index:

Terminal window
icp canister logs <canister-name> -e ic --since-index 100 --until-index 200

Timestamp and index filters cannot be combined with --follow.

To output logs as JSON for programmatic processing:

Terminal window
icp canister logs <canister-name> -e ic --json

By default, only the canister’s controllers can read its logs. You can make logs visible to everyone, or grant read access to specific principals.

Terminal window
icp canister settings update <canister-name> -e ic --log-visibility public

To revert to controller-only visibility:

Terminal window
icp canister settings update <canister-name> -e ic --log-visibility controllers

To allow a principal to view logs without making them public:

Terminal window
icp canister settings update <canister-name> -e ic \
--add-log-viewer <principal-id>

To replace the current set of allowed viewers with a single principal:

Terminal window
icp canister settings update <canister-name> -e ic \
--set-log-viewer <principal-id>

To revoke access for a principal:

Terminal window
icp canister settings update <canister-name> -e ic \
--remove-log-viewer <principal-id>

You can configure log visibility per canister in icp.yaml so it is applied on every icp deploy:

canisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
settings:
log_visibility: controllers # "controllers" | "public" | allowed_viewers object

To grant access to specific principals in the config:

settings:
log_visibility:
allowed_viewers:
- "aaaaa-aa"
- "2vxsx-fae"

The default log buffer size is 4096 bytes. When the buffer fills up, older log entries are automatically purged to make room for new ones. You can increase the limit up to 2 MiB:

Terminal window
icp canister settings update <canister-name> -e ic --log-memory-limit 2mib

Supported suffixes: kb (1,000 bytes), kib (1,024 bytes), mb (1,000,000 bytes), mib (1,048,576 bytes). In icp.yaml:

settings:
log_memory_limit: 2mib

When a canister traps, ICP records a backtrace: the function call stack at the point of the trap: and appends it to the canister logs. If the caller has log access, the backtrace also appears in the error response they receive.

For example, if a Rust canister performs an out-of-bounds stable memory write:

#[update]
fn outer() {
inner();
}
fn inner() {
inner_2();
}
fn inner_2() {
// Note: `ic_cdk::api::stable` is deprecated since ic-cdk 0.18.0.
// Use `ic_cdk::stable::stable_write` instead.
ic_cdk::api::stable::stable_write(0xdeadbeef, b"foo");
}

The log will contain output similar to:

Canister Backtrace:
ic0::ic0::stable64_write
_wasm_backtrace_canister::inner_2
_wasm_backtrace_canister::inner
_wasm_backtrace_canister::outer

This pinpoints that the trap occurred in inner_2, called via outerinner.

Backtraces require function names to be stored in the Wasm name custom section. Any canister built with the standard icp-cli recipes includes this section automatically.

If you post-process the Wasm with ic-wasm (for example to shrink or optimize it), pass --keep-name-section to preserve function names:

Terminal window
ic-wasm canister.wasm -o canister.wasm shrink --keep-name-section
ic-wasm canister.wasm -o canister.wasm optimize O2 --keep-name-section

This requires ic-wasm version 0.8.6 or later.

To verify the name section is present in a Wasm binary, use wasm-objdump and look for a Custom section named "name":

Terminal window
wasm-objdump -h canister.wasm

You should see a line like:

Custom start=0x001e3467 end=0x001e60a6 (size=0x00002c3f) "name"

If the "name" section is absent, backtraces will not be available.

Each canister exposes cumulative statistics about its query call traffic. These are available through the management canister’s canister_status method.

The statistics are cumulative since the canister was created. They are updated approximately once per epoch rather than in real time.

Rust: read query stats from canister_status:

use ic_cdk::{management_canister, update};
use ic_cdk::management_canister::CanisterIdRecord;
#[update]
async fn print_query_stats() -> String {
let status = management_canister::canister_status(
&CanisterIdRecord { canister_id: ic_cdk::id() }
)
.await
.expect("canister_status failed");
let qs = &status.query_stats;
format!(
"calls: {} | instructions: {} | request bytes: {} | response bytes: {}",
qs.num_calls_total,
qs.num_instructions_total,
qs.request_payload_bytes_total,
qs.response_payload_bytes_total,
)
}

Motoko: call canister_status on the management canister:

import Principal "mo:core/Principal";
persistent actor QueryStats {
transient let IC = actor "aaaaa-aa" : actor {
canister_status : { canister_id : Principal } -> async {
query_stats : {
num_calls_total : Nat;
num_instructions_total : Nat;
request_payload_bytes_total : Nat;
response_payload_bytes_total : Nat;
};
};
};
public func get_query_stats() : async Text {
let stats = await IC.canister_status({
canister_id = Principal.fromActor(QueryStats);
});
let qs = stats.query_stats;
"calls: " # debug_show(qs.num_calls_total)
# " | instructions: " # debug_show(qs.num_instructions_total)
# " | request bytes: " # debug_show(qs.request_payload_bytes_total)
# " | response bytes: " # debug_show(qs.response_payload_bytes_total)
};
};
FieldDescription
num_calls_totalTotal number of query calls made to the canister
num_instructions_totalTotal instructions executed across all query calls
request_payload_bytes_totalTotal bytes of query call request payloads
response_payload_bytes_totalTotal bytes of query call response payloads

These cumulative totals accumulate since the canister was created.

Streaming access logs from API boundary nodes

Section titled “Streaming access logs from API boundary nodes”

API boundary nodes (API BNs) handle all incoming requests and log every request they process. You can stream these access logs in real time for a canister. This is especially useful for observing query call traffic, which is otherwise not visible in canister logs.

A complete working implementation in Rust is available at dfinity/ic-bn-logs.

Each access log entry is a JSON object. Key fields:

FieldDescription
ic_canister_idPrincipal ID of the target canister
ic_methodCanister method that was called
request_typequery, call, sync_call, or read_state
http_statusHTTP response code
durationRequest processing time in seconds
timestampUTC timestamp (ISO 8601 with nanosecond precision)
cache_statusHIT, MISS, BYPASS, or DISABLED
error_causeError category if the request failed
client_idSalted hash of client IP + sender principal

Example entry:

{
"cache_status": "DISABLED",
"client_id": "ab6e7b821eb97295e3d20cec94160288",
"duration": 0.028693668,
"http_status": 200,
"ic_canister_id": "qoctq-giaaa-aaaaa-aaaea-cai",
"ic_method": "http_request",
"request_type": "query",
"response_size": 2818,
"timestamp": "2025-07-17T08:12:39.964131788Z"
}

API BNs expose access logs over WebSocket. The URL format is:

wss://{api_bn_domain}/logs/canister/{canister_id}

For full coverage, connect to all API BNs: each node only streams the requests it handles, and traffic is distributed across nodes.

To discover the current list of API BN domains, fetch them from the IC’s certified state using agent-rs:

use candid::Principal;
use ic_agent::Agent;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let agent = Agent::builder()
.with_url("https://icp-api.io")
.build()?;
let subnet_id = Principal::from_text(
"tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe"
)?;
let api_bns = agent
.fetch_api_boundary_nodes_by_subnet_id(subnet_id)
.await?;
for node in &api_bns {
println!("wss://{}/logs/canister/<canister_id>", node.domain);
}
Ok(())
}
  • Canister lifecycle: configure log visibility and memory limits when creating or deploying a canister
  • Testing strategies: use canister logs as part of your debugging workflow
  • CLI reference: full documentation for icp canister logs and icp canister settings update