Documentation

Timers in Stateful Cloud Code

Use timers in Stateful Cloud Code modules to schedule delayed function calls.
Read time 6 minutesLast updated 2 days ago

Use timers to schedule time-based game mechanics in your Stateful Cloud Code modules without managing infrastructure or maintaining persistent connections from game clients. A timer registers a one-off callback that executes a specified Cloud Code function after a defined time span elapses.
Timers are server authoritative and persist within the scope of your module. When a timer elapses, the Cloud Code runtime invokes the target function with the arguments you specified at registration.
Common use cases for timers include the following:
  • Turn timers: Enforce time limits for player turns in multiplayer games.
  • Cooldowns: Restrict how frequently players can perform certain actions.
  • Delayed events: Trigger in-game events after a specified duration.
  • Idle mechanics: Process offline progression or resource generation at intervals.

Timer registration

To use timers, inject
ITimerService
into your module's constructor. The runtime provides the timer service automatically. The target Cloud Code function must be in the same Cloud Code module as the function that registers the timer.
using System;using System.Collections.Generic;using System.Threading.Tasks;using Unity.Services.CloudCode.Core;[StateScope(Scope.MultiplayerSession)]public class RegisterTimerExample{ private readonly ITimerService _timerService; public RegisterTimerExample(ITimerService timerService) { _timerService = timerService; } [CloudCodeFunction("PerformDelayedAction")] public async Task<bool> PerformDelayedAction(IExecutionContext context) { // Register a timer that calls "DelayedAction" after 30 seconds await _timerService.RegisterTimerAsync( TimeSpan.FromSeconds(30), "DelayedAction", new Dictionary<string, object> { { "action", "NOTHING" } } ); return true; } [CloudCodeFunction("DelayedAction")] public async Task DelayedAction(string action) { // Perform your action here }}
In this example, calling
PerformDelayedAction
registers a timer that automatically calls
DelayedAction
after 30 seconds.
PerformDelayedAction
also passes in the relevant argument (in this case,
action
) to the callback function.
You can also accept
ITimerService
as a parameter on an individual Cloud Code function instead of injecting it into the constructor. Use the constructor when more than one function in the module registers timers.
Note
Register
and
Fetch
still work, but they're obsolete. Migrate to
RegisterTimerAsync
and
GetTimerAsync
.

Timer parameters

The
RegisterTimerAsync
method accepts the following parameters:

Parameter

Type

Description

delay
TimeSpan
The duration to wait before the timer elapses.
functionName
string
The name of the Cloud Code function to call when the timer elapses.
arguments
Dictionary<string, object>
An optional dictionary containing arguments to pass to the target function.

Timer limits

Timers have the following limits:
  • Register timers with a delay between 1 second and 24 hours.
  • A module can run up to 10 timers at the same time.
  • A timer callback must not await a call into another module in the same scope. Calls into the same scope run one at a time, so the call waits for the callback and the callback waits for the call. Refer to Avoid calls into your own scope.

Turn timer implementation

You can implement recurring turn timers by having the function called by the timer register its own timer. This pattern is useful for turn-based games where each player has a fixed amount of time to act.
using System;using System.Collections.Generic;using System.Threading.Tasks;using Unity.Services.CloudCode.Core;[StateScope(Scope.MultiplayerSession)]public class TurnBasedGameExample{ public List<string> _players = new List<string>(); public int _currentPlayerIndex; public bool _gameActive; public string? _timerId; private readonly ITimerService _timerService; public TurnBasedGameExample(ITimerService timerService) { _timerService = timerService; } [CloudCodeFunction("StartGame")] public async Task<string> StartGame(List<string> playerIds) { _players = playerIds; _currentPlayerIndex = 0; _gameActive = true; // Start the first turn timer _timerId = await _timerService.RegisterTimerAsync( TimeSpan.FromSeconds(60), "OnTurnTimeout", new Dictionary<string, object> { { "playerId", _players[_currentPlayerIndex] } } ); return $"Game started. {_players[_currentPlayerIndex]}'s turn."; } [CloudCodeFunction("MakeMove")] public async Task<string> MakeMove(IExecutionContext context, string move) { if (_players[_currentPlayerIndex] != context.PlayerId) { return "Not your turn."; } if (_timerId != null) { var timer = await _timerService.GetTimerAsync(_timerId); timer.Cancel(); } // Process the move here // Advance to the next player and register a new turn timer _currentPlayerIndex = (_currentPlayerIndex + 1) % _players.Count; _timerId = await _timerService.RegisterTimerAsync( TimeSpan.FromSeconds(60), "OnTurnTimeout", new Dictionary<string, object> { { "playerId", _players[_currentPlayerIndex] } } ); return $"Move '{move}' processed. Next player's turn."; } [CloudCodeFunction("OnTurnTimeout")] public async Task<string> OnTurnTimeout(string playerId) { // playerId is the argument passed to the timer when it was registered if (!_gameActive || _players[_currentPlayerIndex] != playerId) { return "Timer no longer valid."; } // Handle timeout (skip turn, apply penalty, etc.) var skippedPlayer = playerId; // Advance to the next player and register a new turn timer _currentPlayerIndex = (_currentPlayerIndex + 1) % _players.Count; _timerId = await _timerService.RegisterTimerAsync( TimeSpan.FromSeconds(60), "OnTurnTimeout", new Dictionary<string, object> { { "playerId", _players[_currentPlayerIndex] } } ); return $"Player {skippedPlayer} timed out. Next player's turn."; }}
In this pattern, each call to
OnTurnTimeout
or
MakeMove
advances to the next player and registers a new timer. The timer chain continues until the game ends. Because each registration includes a
playerId
argument, the runtime passes it to
OnTurnTimeout
as the matching
string playerId
parameter. Each argument key you provide when you register a timer must match a parameter name on the target function. Because a module can run up to 10 timers at the same time, keep only the active turn timer and cancel timers that are no longer needed.
Note
This pattern advances turn state, but doesn't tell clients that a turn changed. Refer to Turn change notification patterns to notify the next player instead of leaving their client to poll.

Acting on timers

Sometimes, you might need to cancel a timer before it elapses, for example, when a player completes their turn before the timeout. You might also need to make checks based on a timer's remaining time.
To implement either of these scenarios, fetch the running timer by ID with
ITimerService.GetTimerAsync()
, then call methods on the returned timer. Save the timer ID when you register the timer.
using System;using System.Threading.Tasks;using Unity.Services.CloudCode.Core;[StateScope(Scope.MultiplayerSession)]public class TimerActionExample{ // Marked serializable so the timer ID persists between function invocations. [CloudCodeSerializeProperty] private string? _activeTimerId; private readonly ITimerService _timerService; public TimerActionExample(ITimerService timerService) { _timerService = timerService; } [CloudCodeFunction("StartAction")] public async Task<string> StartAction() { // Register returns a timer ID _activeTimerId = await _timerService.RegisterTimerAsync( TimeSpan.FromSeconds(30), "DoSomething" // Callback function not included below ); return "Action started. Complete within 30 seconds."; } [CloudCodeFunction("UserCompleteAction")] public async Task<string> UserCompleteAction() { // Cancel the timer since the action completed in time if (!string.IsNullOrEmpty(_activeTimerId)) { var timer = await _timerService.GetTimerAsync(_activeTimerId); timer.Cancel(); _activeTimerId = null; } return "Action completed successfully."; } [CloudCodeFunction("RemainingTime")] public async Task<TimeSpan> RemainingTime() { // Return how much time is left on the active timer if (string.IsNullOrEmpty(_activeTimerId)) { throw new InvalidOperationException("Cannot return remaining time because no timer is currently active."); } var timer = await _timerService.GetTimerAsync(_activeTimerId); return timer.RemainingTime(); }}
Tip
Because a module can only run up to 10 timers at the same time, cancel completed or obsolete timers to free timer slots.