Networking & Multiplayer

Host Migration

In a lobby one player is the host. When they leave, the game is handed to another player and everyone carries on. This is on by default and needs no code.

If the host crashes or loses connection there is nothing to hand over, so the game ends for everyone.

This changes the default host-leave behaviour: a graceful departure now transfers the game state to another player. Set Destroy Lobby When Host Leaves to end the session instead. AutoSwitchToBestHost is now a no-op; hosts are no longer switched automatically while they are still playing.

State moves to the new host. Code running on the old host does not.

C#
// Fine. The value is state, the new host has it and keeps counting down.
[Sync] public TimeUntil RoundEnds { get; set; }

protected override void OnUpdate()
{
	if ( Networking.IsHost && RoundEnds )
		EndRound();
}
C#
// Not fine. The value is state, but the task waiting on it is code running on the old host.
// When they leave, nothing ends the round.
[Sync] public TimeUntil RoundEnds { get; set; }

protected override void OnStart()
{
	if ( Networking.IsHost && RoundEnds )
	{
		_ = EndRoundLater();
	}
}

async Task EndRoundLater()
{
	await GameTask.DelaySeconds( RoundEnds );
	EndRound();
}

The same goes for a plain field instead of [Sync], an Invoke, or a static. If it isn't in the snapshot, the new host doesn't have it.

What Happens

  1. The leaving host picks the longest connected player and tells everyone.
  2. It sends that player a snapshot of the game and waits for them to confirm. Everything sent before it, RPCs included, arrives first.
  3. The new host applies the snapshot to its running scene. It gets OnBecameHost, then OnDisconnected for the player who left.
  4. Everyone else brings their networked objects in line with the new host and gets OnHostChanged.

Inside OnBecameHost, the previous host is still in the connection list. After that callback returns, the previous host is removed and OnDisconnected runs. Do not treat their presence during OnBecameHost as meaning they are staying in the game.

Nobody's scene is reloaded. Local objects, pending Invokes and running code on every remaining machine carry on; only what the old host was doing is gone. OnActive is not called again for players already in the game, and ISceneStartup.OnHostInitialize does not run on the new host.

What Survives

Survives Lost
[Sync] properties, on components and GameObjectSystems Plain fields and properties
[Property] values static fields
Networked objects and their owners Pending Invoke calls on the host
INetworkSnapshot data Running async methods on the host
Time.Now
Connection permissions and replicated ConVars

Anything a snapshot carries survives. Anything that was code running on the old host does not.

Synced values from the snapshot are applied again after OnAwake, OnStart and OnEnabled run, so initialising a [Sync] property there doesn't overwrite what the host had.

Host State In Systems

A dictionary on a GameObjectSystem is the most common host-only state. GameObjectSystem supports [Sync], so this is one attribute if the key is already a SteamId or Guid.

C#
// Lost
Dictionary<long, int> _propsPerPlayer = new();

// Survives
[Sync] public NetDictionary<long, int> PropsPerPlayer { get; set; } = new();

Timers Are State

A pending Invoke is code waiting on the old host, so it dies with it. Keep the deadline in a synced TimeUntil and act on it in OnUpdate.

C#
[Sync] public bool IsEnabled { get; set; } = true;
[Sync] public TimeUntil RespawnAt { get; set; }

void OnPickedUp()
{
	IsEnabled = false;
	RespawnAt = RespawnTime;
}

protected override void OnUpdate()
{
	if ( Networking.IsHost && !IsEnabled && RespawnAt )
		IsEnabled = true;
}

The same goes for async loops that animate doors or platforms. Drive them from OnFixedUpdate off synced state, or the door is stuck with IsMoving set forever.

Sync Connections

A Connection in a plain field means nothing on another machine. Synced, it travels as its id, and ids survive migration.

C#
[Sync] public Connection Killer { get; set; }

Restart Host Loops In OnBecameHost

Anything the host was doing, rather than storing, has to be started again.

C#
public void OnBecameHost( Connection previousHost )
{
	// The round clock is [Sync] and kept ticking, so pick up where it is
	_ = RunRoundLoop();
}

A plain "previous state" field used to detect transitions resets too. Sync it, or the transition fires again.

Warnings

The analyzers that ship with the engine warn about these patterns in Visual Studio and Rider. They're off for projects that destroy the lobby when the host leaves.

Id Catches
SB3002 An unsynced Connection stored on a component or system
SB3003 A plain TimeSince or TimeUntil used by host-gated code
SB3004 Invoke scheduled by host-only code
SB3005 A [Sync] property written after an await
SB3006 An await or async/task-returning call in host-only code

SB3006 catches the EndRoundLater() call above, even when the helper does not directly write a synced property. It recognizes host checks, authority checks (!IsProxy) and [Rpc.Host] methods. It does not follow arbitrary call chains or assume that all async code is host-only.

Opting Out

Set Destroy Lobby When Host Leaves in the project's networking settings, or DestroyWhenHostLeaves = true on your LobbyConfig. The game then ends when the host leaves.

Testing

While hosting in the editor, pick Migrate host to new instance from the network menu. An instance is spawned, joins, and takes over as host once the editor disconnects. Its log shows Becoming the host. See Testing Multiplayer.

Created 4 Sep 2026
Updated 7 Sep 2026