From 9eca2bb3414991e568648db7b09e8e4025ab69fc Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Fri, 13 Mar 2026 19:16:34 -0400 Subject: [PATCH 1/4] docs: timers guide (one-shot, recurring, upgrade handling, heartbeat migration) --- docs/guides/backends/timers.md | 215 +++++++++++++++++++++++++++++++-- 1 file changed, 202 insertions(+), 13 deletions(-) diff --git a/docs/guides/backends/timers.md b/docs/guides/backends/timers.md index e6da748f..12bd9d56 100644 --- a/docs/guides/backends/timers.md +++ b/docs/guides/backends/timers.md @@ -6,20 +6,209 @@ sidebar: icskills: [] --- -TODO: Write content for this page. +Canisters can schedule code to run automatically after a delay or on a repeating interval — no external cron job required. This guide covers the timer APIs for Rust and Motoko, how system time works, upgrade handling, and when to use heartbeats instead. - -Set up one-shot and periodic timers in canisters. Cover the timer API for both Rust (ic_cdk_timers) and Motoko (Timer module). Explain migration from heartbeats to timers. Common patterns: periodic cleanup, scheduled data aggregation, timed state transitions. Include cycle cost implications. +## System time - -- Portal: building-apps/integrations/periodic-tasks.mdx -- Examples: periodic_tasks (Rust) -- Rust CDK: https://docs.rs/ic-cdk/latest/ic_cdk/ (timers module) +The IC exposes system time as nanoseconds since `1970-01-01` (Unix timestamp). The value is monotonically increasing, even across canister upgrades. - -Also cover ic0.time() behavior and timestamps (portal: time-and-timestamps.mdx) — developers looking for "how does time work on ICP" will land here. +**Rust:** - -- concepts/timers -- how the global timer mechanism works -- guides/canister-management/lifecycle -- timer setup during canister init -- reference/cycles-costs -- timer execution costs +```rust +let now_ns: u64 = ic_cdk::api::time(); +``` + +**Motoko:** + +```motoko +import Time "mo:core/Time"; + +let now_ns : Int = Time.now(); +``` + +System time is the same for all messages in the same round. It does not advance within a single message execution. + +## One-shot timers + +Schedule a function to run once after a delay. + +**Rust** — add `ic-cdk-timers` to `Cargo.toml`: + +```rust +use ic_cdk_timers::TimerId; +use std::time::Duration; + +let timer_id: TimerId = ic_cdk_timers::set_timer( + Duration::from_secs(60), + || ic_cdk::println!("60 seconds have passed"), +); +``` + +To drive an async function from a timer, use `ic_cdk::spawn`: + +```rust +ic_cdk_timers::set_timer(Duration::from_secs(60), || { + ic_cdk::spawn(async { + // async work here + }) +}); +``` + +**Motoko:** + +```motoko +import Timer "mo:core/Timer"; + +func sendReminder() : async () { + // ... +}; + +let timerId : Timer.TimerId = Timer.setTimer(#seconds 60, sendReminder); +``` + +## Recurring timers + +Schedule a function to run repeatedly at a fixed interval. + +**Rust:** + +```rust +use ic_cdk_timers::TimerId; +use std::time::Duration; + +let timer_id: TimerId = ic_cdk_timers::set_timer_interval( + Duration::from_secs(3600), + || ic_cdk::println!("Hourly task running"), +); +``` + +**Motoko:** + +```motoko +import Timer "mo:core/Timer"; + +func cleanup() : async () { + // periodic cleanup logic +}; + +let timerId : Timer.TimerId = Timer.recurringTimer(#seconds 3600, cleanup); +``` + +A duration of `0` in Motoko will only fire once, not repeatedly. + +## Canceling a timer + +Both one-shot and recurring timers can be canceled before they fire. Canceling an already-expired or unrecognized ID is a no-op. + +**Rust:** + +```rust +ic_cdk_timers::clear_timer(timer_id); +``` + +**Motoko:** + +```motoko +Timer.cancelTimer(timerId); +``` + +## Starting timers on canister init + +A common pattern is to start a recurring timer when the canister is first installed: + +**Rust:** + +```rust +#[ic_cdk_macros::init] +fn init() { + ic_cdk_timers::set_timer_interval( + std::time::Duration::from_secs(3600), + || ic_cdk::println!("Hourly task"), + ); +} +``` + +See [Canister lifecycle](../canister-management/lifecycle.md) for init and upgrade hook details. + +## Timers after upgrades + +**Timers do not survive canister upgrades.** When a canister is upgraded, its Wasm state is replaced and all pending timers are cleared. + +To resume timers after an upgrade, re-register them in `post_upgrade`: + +**Rust:** + +```rust +#[ic_cdk_macros::post_upgrade] +fn post_upgrade() { + // Re-register the same timers as in init + ic_cdk_timers::set_timer_interval( + std::time::Duration::from_secs(3600), + || ic_cdk::println!("Hourly task"), + ); +} +``` + +**Motoko:** + +Motoko's `Timer` module handles the scheduling mechanism. If you need state from before the upgrade to configure timers (such as a stored interval), read it from stable variables in `postupgrade`: + +```motoko +import Timer "mo:core/Timer"; + +persistent actor { + var intervalSecs : Nat = 3600; + + system func postupgrade() { + ignore Timer.recurringTimer(#seconds intervalSecs, periodicTask); + }; +}; +``` + +> Pre- and post-upgrade hooks are error-prone. Avoid them when possible. If your timer interval is fixed, simply re-register it unconditionally in `postupgrade` rather than saving timer IDs to stable memory. + +## Cycle cost implications + +Each timer execution is implemented as a self-canister call. Normal inter-canister call costs apply to each invocation. The [periodic_tasks example](https://github.com/dfinity/examples/tree/master/rust/periodic_tasks) benchmarks timers vs heartbeats and shows timers are more cost-effective than heartbeats for infrequent tasks. + +Timer tasks are added to the canister's input queue. If the canister or subnet is under load, actual execution may be delayed beyond the requested interval. The timer interval is a minimum, not a guarantee. + +The canister output queue is limited to 500 messages. This caps how many timers can fire in a single round. + +See [Cycles and costs](../../reference/cycles-costs.md) for current pricing. + +## Heartbeats (legacy) + +Heartbeats call `canister_heartbeat` on every subnet block (~1 second). They predate timers and have significant drawbacks: + +- Fixed ~1s interval — cannot be adjusted +- Run every block regardless of whether work is needed — burns cycles continuously +- Cannot be disabled without upgrading to remove the export + +**Prefer timers for all new code.** Heartbeats are only appropriate when you need sub-second execution or must respond to every block unconditionally. + +To migrate from heartbeats to timers: +1. Remove the `canister_heartbeat` export (or `system func heartbeat` in Motoko) +2. Register a recurring timer with your desired interval in `init` and `postupgrade` +3. Move the heartbeat logic into the timer callback + +## How the timer mechanism works + +The IC protocol supports one global timer per canister via the `ic0.global_timer_set()` system API and a `canister_global_timer` handler. + +The CDK timers library (`ic-cdk-timers` for Rust, `mo:core/Timer` for Motoko) builds multiple and periodic timers on top of this single protocol timer: + +1. Keeps a global list of all scheduled tasks in the canister heap +2. Calls `ic0.global_timer_set()` to schedule the next upcoming task +3. In `canister_global_timer`, runs each expired task as a self-canister call to isolate tasks from each other and from the library code +4. Reschedules recurring tasks at the end of their execution + +For protocol internals, see [Timers](../../concepts/timers.md) and the [IC specification](https://learn.internetcomputer.org). + +## Full example + +For a complete working example with cycle tracking and multiple timers: + +- [Rust periodic tasks example](https://github.com/dfinity/examples/tree/master/rust/periodic_tasks) + + From 22f0212d14c66bbb4fbcf5d33349533abd6e0d57 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 14:43:20 +0100 Subject: [PATCH 2/4] fix: address review feedback and update to ic-cdk-timers v1.0.0 API Review feedback (marc0olo): - Add "Common patterns" section (cleanup, data aggregation, state transitions) - Add FAQ section from portal (DTS, await behavior, overlapping timers) - Fix heartbeat interval wording to match portal ("close to finalization rate") - Add time conversion packages (Motoko mops, Rust crates) - Add limitations section (relative time only, security warning, resolution) - Add "What's next" links to lifecycle, concepts, and costs pages Source-verified fixes (from .sources/cdk-rs and .sources/motoko-core): - Update all Rust snippets to ic-cdk-timers v1.0.0 API: set_timer takes a future (async {}), set_timer_interval takes FnMut() -> Future (|| async {}) - Remove obsolete ic_cdk::spawn pattern (no longer needed) - Add set_timer_interval_serial for state-mutating periodic tasks - Add idempotency warning and CDK rate limits (250 global, 5 per timer) - Verify Motoko postupgrade spelling and duration 0 behavior against source --- docs/guides/backends/timers.md | 80 ++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/docs/guides/backends/timers.md b/docs/guides/backends/timers.md index 12bd9d56..5f6ceebb 100644 --- a/docs/guides/backends/timers.md +++ b/docs/guides/backends/timers.md @@ -40,19 +40,11 @@ use std::time::Duration; let timer_id: TimerId = ic_cdk_timers::set_timer( Duration::from_secs(60), - || ic_cdk::println!("60 seconds have passed"), + async { ic_cdk::println!("60 seconds have passed") }, ); ``` -To drive an async function from a timer, use `ic_cdk::spawn`: - -```rust -ic_cdk_timers::set_timer(Duration::from_secs(60), || { - ic_cdk::spawn(async { - // async work here - }) -}); -``` +`set_timer` takes a future directly — no closure or `ic_cdk::spawn` wrapper needed. **Motoko:** @@ -78,10 +70,12 @@ use std::time::Duration; let timer_id: TimerId = ic_cdk_timers::set_timer_interval( Duration::from_secs(3600), - || ic_cdk::println!("Hourly task running"), + || async { ic_cdk::println!("Hourly task running") }, ); ``` +`set_timer_interval` takes a closure that returns a future (`|| async { ... }`), not a plain closure. + **Motoko:** ```motoko @@ -94,7 +88,18 @@ func cleanup() : async () { let timerId : Timer.TimerId = Timer.recurringTimer(#seconds 3600, cleanup); ``` -A duration of `0` in Motoko will only fire once, not repeatedly. +A duration of `0` in Motoko will only expire once, not repeatedly (see [Timer.mo](https://github.com/caffeinelabs/motoko-core/blob/v2.1.0/src/Timer.mo#L53)). + +For recurring tasks that mutate state, use `set_timer_interval_serial` in Rust to prevent concurrent invocations — if the interval fires while the previous invocation is still running, the new one is skipped: + +```rust +ic_cdk_timers::set_timer_interval_serial( + Duration::from_secs(3600), + async || { + // safe to mutate state — only one invocation runs at a time + }, +); +``` ## Canceling a timer @@ -112,6 +117,13 @@ ic_cdk_timers::clear_timer(timer_id); Timer.cancelTimer(timerId); ``` +## Common patterns + +- **Periodic cleanup** — purge expired cache entries, remove stale sessions, or compact data structures on a fixed schedule. +- **Scheduled data aggregation** — periodically fetch exchange rates, collect metrics, or roll up statistics from child canisters. +- **Timed state transitions** — expire auctions, unlock funds after a vesting period, or transition a proposal from "voting" to "decided" after a deadline. +- **Heartbeat-to-timer migration** — replace a `canister_heartbeat` export with a recurring timer at the desired interval (see [Heartbeats](#heartbeats-legacy) below). + ## Starting timers on canister init A common pattern is to start a recurring timer when the canister is first installed: @@ -123,7 +135,7 @@ A common pattern is to start a recurring timer when the canister is first instal fn init() { ic_cdk_timers::set_timer_interval( std::time::Duration::from_secs(3600), - || ic_cdk::println!("Hourly task"), + || async { ic_cdk::println!("Hourly task") }, ); } ``` @@ -144,7 +156,7 @@ fn post_upgrade() { // Re-register the same timers as in init ic_cdk_timers::set_timer_interval( std::time::Duration::from_secs(3600), - || ic_cdk::println!("Hourly task"), + || async { ic_cdk::println!("Hourly task") }, ); } ``` @@ -171,17 +183,17 @@ persistent actor { Each timer execution is implemented as a self-canister call. Normal inter-canister call costs apply to each invocation. The [periodic_tasks example](https://github.com/dfinity/examples/tree/master/rust/periodic_tasks) benchmarks timers vs heartbeats and shows timers are more cost-effective than heartbeats for infrequent tasks. -Timer tasks are added to the canister's input queue. If the canister or subnet is under load, actual execution may be delayed beyond the requested interval. The timer interval is a minimum, not a guarantee. +Timer tasks are added to the canister's input queue. If the canister or subnet is under load, actual execution may be delayed beyond the requested interval, and timeouts may result in duplicate execution. The timer interval is a minimum, not a guarantee. Make interval timer callbacks **idempotent** with respect to canister state to handle this safely. -The canister output queue is limited to 500 messages. This caps how many timers can fire in a single round. +The canister output queue is limited to 500 messages. This caps how many timers can fire in a single round. The CDK also enforces internal rate limits (250 concurrent timer calls globally, 5 per interval timer). See [Cycles and costs](../../reference/cycles-costs.md) for current pricing. ## Heartbeats (legacy) -Heartbeats call `canister_heartbeat` on every subnet block (~1 second). They predate timers and have significant drawbacks: +Heartbeats call `canister_heartbeat` at intervals close to the blockchain finalization rate (~1s). They predate timers and have significant drawbacks: -- Fixed ~1s interval — cannot be adjusted +- Fixed interval close to block rate — cannot be adjusted - Run every block regardless of whether work is needed — burns cycles continuously - Cannot be disabled without upgrading to remove the export @@ -205,10 +217,40 @@ The CDK timers library (`ic-cdk-timers` for Rust, `mo:core/Timer` for Motoko) bu For protocol internals, see [Timers](../../concepts/timers.md) and the [IC specification](https://learn.internetcomputer.org). +## Frequently asked questions + +**Do timers support deterministic time slicing (DTS)?** +Yes. Each timer executes as a self-canister call, so normal update message instruction limits apply with DTS enabled. + +**What happens if a timer handler awaits an inter-canister call?** +Normal await point rules apply — any new execution can start at the await point (a new message, another timer, or a heartbeat). The current timer handler resumes after the new execution finishes or reaches its own await point. + +**What happens if a periodic timer takes longer than its interval?** +With `set_timer_interval`, multiple invocations can run concurrently. With `set_timer_interval_serial`, the new invocation is skipped if the previous one is still running. If there are no await points, the timer is rescheduled after execution completes. + +## Time conversion + +System time is returned in nanoseconds. For DateTime conversions, use these packages: + +- **Motoko:** [`time`](https://mops.one/time) (milliseconds, string format) and [`dateTime`](https://mops.one/datetime) (UTC, local timezone) +- **Rust:** [`time`](https://time-rs.github.io/api/time/index.html) and [`datetimeutils`](https://crates.io/crates/datetimeutils) + +## Limitations + +- Timer resolution is similar to the block rate — choose durations well above ~1s. +- The CDK timers library uses **relative time** only. To schedule at an absolute time, calculate the duration from `now` to the target time manually. +- Using timers for security (e.g., access control) is strongly discouraged. Timers vanish on upgrades and reinstalls, and reentrancy can undermine access checks. + ## Full example For a complete working example with cycle tracking and multiple timers: - [Rust periodic tasks example](https://github.com/dfinity/examples/tree/master/rust/periodic_tasks) - +## What's next + +- [Canister lifecycle](../canister-management/lifecycle.md) — init, pre/post-upgrade hooks +- [Timers (concept)](../../concepts/timers.md) — how the IC protocol timer works +- [Cycles and costs](../../reference/cycles-costs.md) — current pricing + + From 21bf40613bafa1678a5587fdc9a03f2ee9d710fc Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 14:46:39 +0100 Subject: [PATCH 3/4] fix: correct system time guarantee (per-message, not per-round) The previous wording claimed system time is the same for all messages in a round. The actual guarantee from motoko-core Time.mo:42 is that time is constant within a single message execution only. --- docs/guides/backends/timers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/backends/timers.md b/docs/guides/backends/timers.md index 5f6ceebb..a6259a05 100644 --- a/docs/guides/backends/timers.md +++ b/docs/guides/backends/timers.md @@ -26,7 +26,7 @@ import Time "mo:core/Time"; let now_ns : Int = Time.now(); ``` -System time is the same for all messages in the same round. It does not advance within a single message execution. +System time is constant within a single message execution — it does not advance mid-call. Different messages in the same round may observe different timestamps. ## One-shot timers From 02b3cbab377ef11ee74f9d8ba40e0f9a99c43884 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Mon, 16 Mar 2026 15:20:31 +0100 Subject: [PATCH 4/4] fix: link to internal IC spec page instead of Learn Hub --- docs/guides/backends/timers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/backends/timers.md b/docs/guides/backends/timers.md index a6259a05..54b8b80d 100644 --- a/docs/guides/backends/timers.md +++ b/docs/guides/backends/timers.md @@ -215,7 +215,7 @@ The CDK timers library (`ic-cdk-timers` for Rust, `mo:core/Timer` for Motoko) bu 3. In `canister_global_timer`, runs each expired task as a self-canister call to isolate tasks from each other and from the library code 4. Reschedules recurring tasks at the end of their execution -For protocol internals, see [Timers](../../concepts/timers.md) and the [IC specification](https://learn.internetcomputer.org). +For protocol internals, see [Timers](../../concepts/timers.md) and the [IC interface specification](../../reference/ic-interface-spec.md). ## Frequently asked questions