Skip to main content

Overview

Grain timers let a grain schedule messages to itself: once after a delay, repeatedly at a fixed interval, or on a cron expression. Ticks are delivered through the grain’s mailbox and processed like any other message, serialized with the rest of the grain’s messages, with ctx.Message() returning the scheduled message. Timers are volatile and activation-scoped. They are all cancelled when the grain deactivates, they are never persisted, and they never reactivate a passivated grain. Use them for work that only matters while the grain is in memory: heartbeats, batch flushing, cache refresh, timeouts, and polling.
Grain timers are self-scheduling only. To schedule messages to actors from anywhere in the system, use the actor scheduler. Durable, reactivating schedules for grains (reminders) are not available.

API

The same four methods are available on GrainContext (inside OnReceive) and on GrainProps (inside OnActivate and OnDeactivate): Every schedule call returns the timer’s reference, the handle used to cancel it.

Timer Options

Starting a Timer From OnActivate

Registering timers in OnActivate is the canonical way to start a grain’s periodic behavior. Timers registered there stay dormant until activation completes, so a short delay cannot fire into a grain that is not ready yet, and they are discarded if activation fails.

Scheduling From OnReceive

A grain can schedule and cancel timers while handling messages, for example to implement a timeout:
Timers do not survive passivation. A grain with a pending 30-minute one-shot passivates on its normal schedule and the timer is lost. If the grain must stay available for the timer, register it with WithTimerKeepAlive() or extend the grain’s passivation timeout.

Semantics

Lifecycle

  • Timers registered during OnActivate stay dormant until activation completes and are discarded when activation fails.
  • All timers are cancelled when the grain deactivates: passivation, system shutdown, or a failure. OnDeactivate runs after the timers are stopped, so scheduling from that hook returns ErrGrainTimersStopped.
  • A tick that is already in the mailbox when its timer is cancelled, or when the grain deactivates, is dropped instead of delivered.
  • After reactivation a grain starts with no timers; re-register them in OnActivate.

Delivery

  • Ticks are processed one at a time, serialized with the grain’s other messages. Tick handling never runs concurrently with other work on the same grain.
  • Schedule fires at a fixed cadence, matching the actor scheduler. A handler slower than the interval accumulates queued ticks in the mailbox; execution itself never overlaps. This differs from Orleans, where the period is measured from callback completion.
  • Tick handlers run with a background context: no deadline and no cancellation.
  • A tick is fire-and-forget: an error reported with ctx.Err(err) is logged as a warning, and ctx.Unhandled() is logged the same way. Signal at most once per message (Err, NoErr, or Unhandled), as with any Tell-style message.
  • When the mailbox is full (bounded mailboxes only), the tick is dropped and logged; interval and cron timers keep firing.

Passivation

  • By default a tick does not reset the grain’s passivation clock: a grain that only receives timer ticks still passivates on schedule. Opt in per timer with WithTimerKeepAlive().
  • Delivered ticks count as processed messages, so message-count-based passivation strategies see them.

Cron

  • Cron expressions use the Quartz format, evaluated in the process’s local timezone.
  • Unlike ActorSystem.ScheduleWithCron in cluster mode, no explicit reference is required and no cluster-wide arbitration takes place: a grain has exactly one activation cluster-wide, so each tick fires exactly once by construction.

Errors

Grain Timers vs Actor Scheduling