﻿# Timers

> For the complete documentation index, see [llms.txt](/llms.txt)

[Canisters](../../concepts/canisters.md) 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.

## System time

The IC exposes system time as nanoseconds since `1970-01-01` (Unix timestamp). The value is monotonically increasing, even across canister upgrades.

### Motoko

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

let now_ns : Int = Time.now();
```

### Rust

```rust
let now_ns: u64 = ic_cdk::api::time();
```

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.

### Motoko

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

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

let timerId : Timer.TimerId = Timer.setTimer<system>(#seconds 60, sendReminder);
```

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

## Recurring timers

Schedule a function to run repeatedly at a fixed interval.

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

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

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.

### Motoko

```motoko
Timer.cancelTimer(timerId);
```

### Rust

```rust
ic_cdk_timers::clear_timer(timer_id);
```

## 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#what-happens-during-an-upgrade) 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`:

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

### 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") },
    );
}
```

> 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](../../references/cycles-costs.md#cost-table) 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](../../references/ic-interface-spec/canister-interface.md#global-timer).

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

## Next steps

- [Canister lifecycle](../canister-management/lifecycle.md#what-happens-during-an-upgrade): init, pre/post-upgrade hooks
- [Timers (concept)](../../concepts/timers.md): how the IC protocol timer works
- [Cycles and costs](../../references/cycles-costs.md#cost-table): current pricing
