Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 244 additions & 13 deletions docs/guides/backends/timers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,251 @@ 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.

<!-- Content Brief -->
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

<!-- Source Material -->
- 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.

<!-- Writing Note -->
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:**

<!-- Cross-Links -->
- 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 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

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),
async { ic_cdk::println!("60 seconds have passed") },
);
```

`set_timer` takes a future directly — no closure or `ic_cdk::spawn` wrapper needed.

**Motoko:**

```motoko
import Timer "mo:core/Timer";

func sendReminder() : async () {
// ...
};

let timerId : Timer.TimerId = Timer.setTimer<system>(#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),
|| async { ic_cdk::println!("Hourly task running") },
);
```

`set_timer_interval` takes a closure that returns a future (`|| async { ... }`), not a plain closure.

**Motoko:**

```motoko
import Timer "mo:core/Timer";

func cleanup() : async () {
// periodic cleanup logic
};

let timerId : Timer.TimerId = Timer.recurringTimer<system>(#seconds 3600, cleanup);
```

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

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);
```

## 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:

**Rust:**

```rust
#[ic_cdk_macros::init]
fn init() {
ic_cdk_timers::set_timer_interval(
std::time::Duration::from_secs(3600),
|| async { 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),
|| async { 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<system>(#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, 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 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` at intervals close to the blockchain finalization rate (~1s). They predate timers and have significant drawbacks:

- 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

**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 interface specification](../../reference/ic-interface-spec.md).

## 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

<!-- Upstream: informed by dfinity/portal docs/building-apps/network-features/periodic-tasks-timers.mdx, docs/building-apps/network-features/time-and-timestamps.mdx, dfinity/cdk-rs ic-cdk-timers/src/lib.rs, and caffeinelabs/motoko-core src/Timer.mo -->
Loading