# Timers in CloudBehaviours

> Schedule, cancel, and understand the limits of a server-authoritative CloudBehaviour timer.

> **Important:**
>
> This page describes an experimental feature that might change significantly before release. It's not recommended to use or rely on experimental features in production environments due to potential instability.

Timers are server-authoritative. The cloud schedules a call back into one of your CloudBehaviour's own endpoints, so a player who closes the game can't dodge a turn clock.

## Schedule a timer

Scheduling uses the same generated `I{Behaviour}` interface used for [cross-behaviour calls](/cloud-code/cloudbehaviours/calling-another-behaviour.md). Inject it, name the endpoint with its real arguments, and run the call with `ScheduleAsync(delay)`:

```csharp title="Scheduling a timer from an RPC"
[StateScope(Scope.MultiplayerSession)]
public class TurnClock : CloudBehaviour
{
    public string CurrentTimerId;
    public int TurnNumber;

    [CloudCodeAccess(Access.SessionHost)]
    public async Task StartTurn([CloudCodeService] ITurnClock self, [CloudCodeService] ITimerService timers)
    {
        if (CurrentTimerId != null)
        {
            var previous = await timers.GetTimerAsync(CurrentTimerId);
            previous.Cancel();   // Don't leave an orphaned timer holding a slot
        }

        CurrentTimerId = await self.EndTurn(TurnNumber).ScheduleAsync(TimeSpan.FromSeconds(30));
    }

    [CloudCodeAccess(Access.Service)]
    public void EndTurn(int turnNumber)
    {
        if (turnNumber != TurnNumber) return;   // A stale timer fires against an old turn
        CurrentTimerId = null;
        TurnNumber++;
    }
}
```

Arguments are ordinary typed parameters on the endpoint. `ScheduleAsync` registers a timer against the deployed endpoint and returns the timer's ID.

`StartTurn` is restricted to the session host. Without `[CloudCodeAccess]`, any session member could call it repeatedly and fill the session's timer slots. It also cancels the previous timer before scheduling a new one, so no orphaned timer is left running. `EndTurn` clears `CurrentTimerId` when the timer fires, because a fired timer's ID no longer resolves: calling `GetTimerAsync` with it throws. For other ways to restrict who calls an endpoint, refer to [Multiplayer game security in CloudBehaviours](/cloud-code/cloudbehaviours/secure-multiplayer-game-logic.md).

To inject `ITurnClock`, register it in your module's `ICloudCodeSetup` with `config.AddTurnClockClient()`. Refer to [Register the generated interface](/cloud-code/cloudbehaviours/rpcs.md#register-the-generated-interface).

## Cancel a timer

`ICall` has no cancel of its own. Inject `[CloudCodeService] ITimerService` to fetch and cancel a timer by ID.

You don't need `ITimerService` to schedule a timer, only to cancel or query one. `ScheduleAsync` uses the timer service for you. Don't call `ITimerService.RegisterTimerAsync` directly in a CloudBehaviour: the build fails with `CCSG137`.

`GetTimerAsync` throws for a timer that has already fired or been cancelled, so clear a stored timer ID in the endpoint the timer calls, as `EndTurn` does in the preceding example.

The following example cancels the current turn's timer:

```csharp title="Canceling a timer"
[CloudCodeAccess(Access.SessionHost)]
public async Task CancelTurn([CloudCodeService] ITimerService timers)
{
    if (CurrentTimerId == null) return;

    var timer = await timers.GetTimerAsync(CurrentTimerId);
    timer.Cancel();
    CurrentTimerId = null;
}
```

### Persist the timer ID

The timer service returns a timer's ID only once, when you schedule it, and it has no way to list your timers later. If you'll need to cancel a timer, store its ID in a persisted member. An ID kept only in memory isn't saved: it can survive between calls, but it's lost whenever Cloud Code reloads your CloudBehaviour, for example after a period of inactivity or a redeploy.

In the preceding examples, `CurrentTimerId` is a public field, so it persists by default, but it also synchronizes to every client in the session. To persist the ID without sending it to clients, make it private and mark it `[CloudCodeSerializeProperty]`:

```csharp title="Persisting a timer ID in the cloud only"
[CloudCodeSerializeProperty]
private string _turnTimerId;   // Persisted, never sent to a client

[CloudCodeAccess(Access.SessionHost)]
public async Task StartTurn([CloudCodeService] ITurnClock self)
{
    _turnTimerId = await self.EndTurn(TurnNumber).ScheduleAsync(TimeSpan.FromSeconds(30));
}

[CloudCodeAccess(Access.SessionHost)]
public async Task CancelTurn([CloudCodeService] ITimerService timers)
{
    if (_turnTimerId == null) return;

    (await timers.GetTimerAsync(_turnTimerId)).Cancel();
    _turnTimerId = null;
}
```

## Lock the target down, and don't expect a player

A scheduled endpoint is still an endpoint, so a client can call it directly. Mark it `[CloudCodeAccess(Access.Service)]` so the timer still fires while clients are blocked.

> **Important:**
>
> A timer-triggered invocation has `Context.PlayerId` set to `null`, and there's no player access token. Use `Context.ServiceToken` for outbound service calls, and pass what the endpoint needs as arguments instead of reading the calling player.

A timer can target only a public instance endpoint on the CloudBehaviour. A private or static method isn't on `I{Behaviour}`, so scheduling one doesn't compile.

### What a timer callback pushes

A timer-triggered invocation pushes synchronized state and declared events to the scope's clients: the player on a `Player`-scoped CloudBehaviour, or every session member on a `MultiplayerSession`-scoped CloudBehaviour. If the CloudBehaviour has [per-player state views](/cloud-code/cloudbehaviours/per-player-state-views.md), each client receives its own view.

## Limits

Timers have the following limits:

* The delay must be greater than zero and no more than 24 hours. The 24 hours are shared: the delays of all timers already pending on the same player or session, plus the new one, can't exceed 24 hours.
* A player or session can hold at most 10 pending timers at once, shared by every module and CloudBehaviour that uses that scope. A timer frees its slot when it fires or when you cancel it.
* There's no recurring timer. For a repeating clock, have the scheduled endpoint schedule the next one.
* Only schedule your own CloudBehaviour's endpoints. Calling `ScheduleAsync` on another CloudBehaviour's `I{Behaviour}` fails the build with `CCSG132`.

## Additional resources

* [Turn-based game workflow](/cloud-code/cloudbehaviours/turn-based-game-workflow.md)
* [RPCs in CloudBehaviours](/cloud-code/cloudbehaviours/rpcs.md)
* [Access control in CloudBehaviours](/cloud-code/cloudbehaviours/access-control.md)
