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 a grain from anywhere in the system, including to a grain that has passivated, use the actor system’s grain scheduling methods; to schedule messages to actors, use the actor scheduler. Neither is persisted: durable reminders that survive a node restart are not available.

API

The same four methods are available on GrainContext (inside OnReceive) and on GrainProps (inside OnActivate and OnDeactivate):
  • ScheduleOnce: Deliver once after the delay.
  • Schedule: Deliver repeatedly at a fixed interval.
  • ScheduleWithCron: Deliver according to a cron expression.
  • CancelSchedule: Cancel a timer.
Every schedule call returns the timer’s reference, the handle used to cancel it.

Timer options

  • WithTimerReference: Set the timer’s reference. References are scoped to the grain; registering under a reference already in use cancels and replaces the existing timer.
  • WithTimerKeepAlive: Make ticks count as passivation activity, keeping the grain alive while the timer runs. By default ticks do not reset the passivation clock.

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.
Starting a Timer From OnActivate

Scheduling from OnReceive

A grain can schedule and cancel timers while handling messages, for example to implement a timeout:
Scheduling From OnReceive
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. If the message must reach the grain even after it has passivated, use a grain schedule instead: it is held by the node’s scheduler and activates the grain again on delivery.

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: the period is measured from the previous fire time, not from the handler’s completion. A handler slower than the interval accumulates queued ticks in the mailbox; execution itself never overlaps.
  • 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

  • ErrGrainTimersStopped: Scheduling or cancelling while the grain is deactivating or not activated.
  • ErrInvalidTimerInterval: Schedule is called with an interval that is not strictly positive.
  • ErrScheduledReferenceNotFound: Cancelling an unknown reference, or a one-shot that has already fired.

Grain timers vs scheduling

Grain timers are one of two ways to deliver a message to a grain later. The other is the actor system’s scheduler, which targets actors through their *PID and grains through their identity; for the grain side of that comparison, see Grain schedules vs grain timers. Against the actor scheduler:
  • Who schedules: Grain timers: The grain itself. Actor scheduler: Any code holding a *PID.
  • Target: Grain timers: Always the scheduling grain. Actor scheduler: Any actor, local or remote.
  • Lifetime: Grain timers: The current activation. Actor scheduler: Until cancelled or the node stops.
  • Wakes the target: Grain timers: Never. Actor scheduler: Delivery fails if the actor is gone.
  • Cluster cron: Grain timers: No arbitration needed (single activation). Actor scheduler: Requires WithReference for single fire.