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.
Writing log messages
Section titled “Writing log messages”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.
What logging captures
Section titled “What logging captures”Log messages are recorded for:
- Update calls
- Timer and heartbeat executions
canister_init,canister_pre_upgrade, andcanister_post_upgradehooks- 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).
Viewing canister logs
Section titled “Viewing canister logs”To fetch and display the logs for a canister:
icp canister logs <canister-name> -e localTo follow logs in real time (polls every 2 seconds by default):
icp canister logs <canister-name> -e local --followTo adjust the polling interval:
icp canister logs <canister-name> -e local --follow --interval 5To fetch logs on mainnet, use -e ic:
icp canister logs <canister-name> -e icFiltering by timestamp or index
Section titled “Filtering by timestamp or index”You can scope log output to a specific time range or index range.
By timestamp (RFC3339 or nanoseconds since Unix epoch):
icp canister logs <canister-name> -e ic \ --since 2024-01-01T00:00:00Z \ --until 2024-01-02T00:00:00ZBy log entry index:
icp canister logs <canister-name> -e ic --since-index 100 --until-index 200Timestamp and index filters cannot be combined with --follow.
To output logs as JSON for programmatic processing:
icp canister logs <canister-name> -e ic --jsonLog visibility
Section titled “Log visibility”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.
Making logs public
Section titled “Making logs public”icp canister settings update <canister-name> -e ic --log-visibility publicTo revert to controller-only visibility:
icp canister settings update <canister-name> -e ic --log-visibility controllersGranting specific principals access
Section titled “Granting specific principals access”To allow a principal to view logs without making them public:
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:
icp canister settings update <canister-name> -e ic \ --set-log-viewer <principal-id>To revoke access for a principal:
icp canister settings update <canister-name> -e ic \ --remove-log-viewer <principal-id>Setting log visibility in icp.yaml
Section titled “Setting log visibility in icp.yaml”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 objectTo grant access to specific principals in the config:
settings: log_visibility: allowed_viewers: - "aaaaa-aa" - "2vxsx-fae"Log memory limit
Section titled “Log memory limit”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:
icp canister settings update <canister-name> -e ic --log-memory-limit 2mibSupported 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: 2mibBacktrace debugging
Section titled “Backtrace debugging”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::outerThis pinpoints that the trap occurred in inner_2, called via outer → inner.
Verifying backtrace support
Section titled “Verifying backtrace support”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:
ic-wasm canister.wasm -o canister.wasm shrink --keep-name-sectionic-wasm canister.wasm -o canister.wasm optimize O2 --keep-name-sectionThis 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":
wasm-objdump -h canister.wasmYou should see a line like:
Custom start=0x001e3467 end=0x001e60a6 (size=0x00002c3f) "name"If the "name" section is absent, backtraces will not be available.
Query statistics
Section titled “Query statistics”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) };};Query statistics fields
Section titled “Query statistics fields”| Field | Description |
|---|---|
num_calls_total | Total number of query calls made to the canister |
num_instructions_total | Total instructions executed across all query calls |
request_payload_bytes_total | Total bytes of query call request payloads |
response_payload_bytes_total | Total 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.
Access log format
Section titled “Access log format”Each access log entry is a JSON object. Key fields:
| Field | Description |
|---|---|
ic_canister_id | Principal ID of the target canister |
ic_method | Canister method that was called |
request_type | query, call, sync_call, or read_state |
http_status | HTTP response code |
duration | Request processing time in seconds |
timestamp | UTC timestamp (ISO 8601 with nanosecond precision) |
cache_status | HIT, MISS, BYPASS, or DISABLED |
error_cause | Error category if the request failed |
client_id | Salted 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"}Connecting to the WebSocket endpoint
Section titled “Connecting to the WebSocket endpoint”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(())}Next steps
Section titled “Next steps”- 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 logsandicp canister settings update