diff --git a/Agent/Phantom.Agent.Minecraft/Server/ServerStatusProtocol.cs b/Agent/Phantom.Agent.Minecraft/Server/ServerStatusProtocol.cs
index c28202c..caa8163 100644
--- a/Agent/Phantom.Agent.Minecraft/Server/ServerStatusProtocol.cs
+++ b/Agent/Phantom.Agent.Minecraft/Server/ServerStatusProtocol.cs
@@ -3,28 +3,11 @@ using System.Buffers.Binary;
 using System.Net;
 using System.Net.Sockets;
 using System.Text;
-using Phantom.Utils.Logging;
-using Serilog;
 
 namespace Phantom.Agent.Minecraft.Server;
 
-public sealed class ServerStatusProtocol {
-	private readonly ILogger logger;
-
-	public ServerStatusProtocol(string loggerName) {
-		this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName);
-	}
-
-	public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) {
-		try {
-			return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
-		} catch (Exception e) {
-			logger.Error(e, "Caught exception while checking if players are online.");
-			return null;
-		}
-	}
-
-	private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) {
+public static class ServerStatusProtocol {
+	public static async Task<int> GetOnlinePlayerCount(ushort serverPort, CancellationToken cancellationToken) {
 		using var tcpClient = new TcpClient();
 		await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
 		var tcpStream = tcpClient.GetStream();
@@ -33,24 +16,22 @@ public sealed class ServerStatusProtocol {
 		tcpStream.WriteByte(0xFE);
 		await tcpStream.FlushAsync(cancellationToken);
 
-		short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
-		return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
+		short messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
+		return await ReadOnlinePlayerCount(tcpStream, messageLength * 2, cancellationToken);
 	}
 
-	private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
+	private static async Task<short> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
 		var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
 		try {
 			await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
 
 			if (headerBuffer[0] != 0xFF) {
-				logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
-				return null;
+				throw new ProtocolException("Unexpected first byte in response from server: " + headerBuffer[0]);
 			}
 
 			short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
 			if (messageLength <= 0) {
-				logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
-				return null;
+				throw new ProtocolException("Unexpected message length in response from server: " + messageLength);
 			}
 			
 			return messageLength;
@@ -59,7 +40,7 @@ public sealed class ServerStatusProtocol {
 		}
 	}
 
-	private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
+	private static async Task<int> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
 		var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
 		try {
 			await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
@@ -74,20 +55,21 @@ public sealed class ServerStatusProtocol {
 			int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte);
 			int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1);
 			if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) {
-				logger.Error("Could not find message separators in response from server.");
-				return null;
+				throw new ProtocolException("Could not find message separators in response from server.");
 			}
 
 			string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer.AsSpan((separator1 + 1)..(separator2 - 1)));
 			if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) {
-				logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr);
-				return null;
+				throw new ProtocolException("Could not parse online player count in response from server: " + onlinePlayerCountStr);
 			}
 			
-			logger.Debug("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount);
 			return onlinePlayerCount;
 		} finally {
 			ArrayPool<byte>.Shared.Return(messageBuffer);
 		}
 	}
+	
+	public sealed class ProtocolException : Exception {
+		internal ProtocolException(string message) : base(message) {}
+	}
 }
diff --git a/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs b/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
index 4ff9a68..82c2d9c 100644
--- a/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
+++ b/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
@@ -1,10 +1,8 @@
-using Phantom.Agent.Minecraft.Instance;
-using Phantom.Agent.Minecraft.Server;
-using Phantom.Agent.Services.Instances;
+using Phantom.Agent.Services.Instances;
+using Phantom.Agent.Services.Instances.State;
 using Phantom.Common.Data.Backups;
 using Phantom.Utils.Logging;
 using Phantom.Utils.Tasks;
-using Phantom.Utils.Threading;
 
 namespace Phantom.Agent.Services.Backups;
 
@@ -16,27 +14,23 @@ sealed class BackupScheduler : CancellableBackgroundTask {
 
 	private readonly BackupManager backupManager;
 	private readonly InstanceContext context;
-	private readonly InstanceProcess process;
 	private readonly SemaphoreSlim backupSemaphore = new (1, 1);
-	private readonly int serverPort;
-	private readonly ServerStatusProtocol serverStatusProtocol;
 	private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
+	private readonly InstancePlayerCountTracker playerCountTracker;
 	
 	public event EventHandler<BackupCreationResult>? BackupCompleted;
 
-	public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
+	public BackupScheduler(InstanceContext context, InstancePlayerCountTracker playerCountTracker) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
 		this.backupManager = context.Services.BackupManager;
 		this.context = context;
-		this.process = process;
-		this.serverPort = serverPort;
-		this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
+		this.playerCountTracker = playerCountTracker;
 		Start();
 	}
 
 	protected override async Task RunTask() {
 		await Task.Delay(InitialDelay, CancellationToken);
 		Logger.Information("Starting a new backup after server launched.");
-			
+		
 		while (!CancellationToken.IsCancellationRequested) {
 			var result = await CreateBackup();
 			BackupCompleted?.Invoke(this, result);
@@ -69,43 +63,18 @@ sealed class BackupScheduler : CancellableBackgroundTask {
 	}
 
 	private async Task WaitForOnlinePlayers() {
-		bool needsToLogOfflinePlayersMessage = true;
-		
-		process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0);
-		try {
-			while (!CancellationToken.IsCancellationRequested) {
-				serverOutputWhileWaitingForOnlinePlayers.Reset();
-				
-				var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
-				if (onlinePlayerCount == null) {
-					Logger.Warning("Could not detect whether any players are online, starting a new backup.");
-					break;
-				}
-
-				if (onlinePlayerCount > 0) {
-					Logger.Information("Players are online, starting a new backup.");
-					break;
-				}
-
-				if (needsToLogOfflinePlayersMessage) {
-					needsToLogOfflinePlayersMessage = false;
-					Logger.Information("No players are online, waiting for someone to join before starting a new backup.");
-				}
-
-				await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
-				
-				Logger.Debug("Waiting for server output before checking for online players again...");
-				await serverOutputWhileWaitingForOnlinePlayers.WaitHandle.WaitOneAsync(CancellationToken);
-			}
-		} finally {
-			process.RemoveOutputListener(ServerOutputListener);
+		var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken);
+		if (!task.IsCompleted) {
+			Logger.Information("Waiting for someone to join before starting a new backup.");
 		}
-	}
-
-	private void ServerOutputListener(object? sender, string line) {
-		if (!serverOutputWhileWaitingForOnlinePlayers.IsSet) {
-			serverOutputWhileWaitingForOnlinePlayers.Set();
-			Logger.Debug("Detected server output, signalling to check for online players again.");
+		
+		try {
+			await task;
+			Logger.Information("Players are online, starting a new backup.");
+		} catch (OperationCanceledException) {
+			throw;
+		} catch (Exception) {
+			Logger.Warning("Could not detect whether any players are online, starting a new backup.");
 		}
 	}
 
diff --git a/Agent/Phantom.Agent.Services/Instances/State/InstancePlayerCountTracker.cs b/Agent/Phantom.Agent.Services/Instances/State/InstancePlayerCountTracker.cs
new file mode 100644
index 0000000..4a81129
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstancePlayerCountTracker.cs
@@ -0,0 +1,132 @@
+using Phantom.Agent.Minecraft.Instance;
+using Phantom.Agent.Minecraft.Server;
+using Phantom.Utils.Logging;
+using Phantom.Utils.Tasks;
+using Phantom.Utils.Threading;
+
+namespace Phantom.Agent.Services.Instances.State;
+
+sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
+	private readonly InstanceProcess process;
+	private readonly ushort serverPort;
+
+	private readonly TaskCompletionSource firstDetection = AsyncTasks.CreateCompletionSource();
+	private readonly ManualResetEventSlim serverOutputEvent = new ();
+
+	private int? onlinePlayerCount;
+
+	public int? OnlinePlayerCount {
+		get {
+			lock (this) {
+				return onlinePlayerCount;
+			}
+		}
+		private set {
+			EventHandler<int?>? onlinePlayerCountChanged;
+			lock (this) {
+				if (onlinePlayerCount == value) {
+					return;
+				}
+				
+				onlinePlayerCount = value;
+				onlinePlayerCountChanged = OnlinePlayerCountChanged;
+			}
+
+			onlinePlayerCountChanged?.Invoke(this, value);
+		}
+	}
+
+	private event EventHandler<int?>? OnlinePlayerCountChanged;
+
+	private bool isDisposed = false;
+
+	public InstancePlayerCountTracker(InstanceContext context, InstanceProcess process, ushort serverPort) : base(PhantomLogger.Create<InstancePlayerCountTracker>(context.ShortName)) {
+		this.process = process;
+		this.serverPort = serverPort;
+		Start();
+	}
+
+	protected override async Task RunTask() {
+		// Give the server time to start accepting connections.
+		await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
+
+		serverOutputEvent.Set();
+		process.AddOutputListener(OnOutput, maxLinesToReadFromHistory: 0);
+		
+		while (!CancellationToken.IsCancellationRequested) {
+			serverOutputEvent.Reset();
+
+			OnlinePlayerCount = await TryGetOnlinePlayerCount();
+			
+			if (!firstDetection.Task.IsCompleted) {
+				firstDetection.SetResult();
+			}
+
+			await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
+			await serverOutputEvent.WaitHandle.WaitOneAsync(CancellationToken);
+			await Task.Delay(TimeSpan.FromSeconds(1), CancellationToken);
+		}
+	}
+
+	private async Task<int?> TryGetOnlinePlayerCount() {
+		try {
+			int newOnlinePlayerCount = await ServerStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
+			Logger.Debug("Detected {OnlinePlayerCount} online player(s).", newOnlinePlayerCount);
+			return newOnlinePlayerCount;
+		} catch (ServerStatusProtocol.ProtocolException e) {
+			Logger.Error(e.Message);
+			return null;
+		} catch (Exception e) {
+			Logger.Error(e, "Caught exception while checking online player count.");
+			return null;
+		}
+	}
+
+	public async Task WaitForOnlinePlayers(CancellationToken cancellationToken) {
+		await firstDetection.Task.WaitAsync(cancellationToken);
+
+		var onlinePlayersDetected = AsyncTasks.CreateCompletionSource();
+
+		lock (this) {
+			if (onlinePlayerCount == null) {
+				throw new InvalidOperationException();
+			}
+			else if (onlinePlayerCount > 0) {
+				return;
+			}
+
+			OnlinePlayerCountChanged += OnOnlinePlayerCountChanged;
+
+			void OnOnlinePlayerCountChanged(object? sender, int? newPlayerCount) {
+				if (newPlayerCount == null) {
+					onlinePlayersDetected.TrySetException(new InvalidOperationException());
+					OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
+				}
+				else if (newPlayerCount > 0) {
+					onlinePlayersDetected.TrySetResult();
+					OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
+				}
+			}
+		}
+
+		await onlinePlayersDetected.Task;
+	}
+
+	private void OnOutput(object? sender, string? line) {
+		lock (this) {
+			if (!isDisposed) {
+				serverOutputEvent.Set();
+			}
+		}
+	}
+
+	protected override void Dispose() {
+		lock (this) {
+			isDisposed = true;
+			onlinePlayerCount = null;
+		}
+
+		process.RemoveOutputListener(OnOutput);
+		serverOutputEvent.Dispose();
+	}
+}
diff --git a/Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs b/Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs
index 1470682..f2a8534 100644
--- a/Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs
@@ -19,6 +19,7 @@ sealed class InstanceRunningState : IDisposable {
 	private readonly CancellationToken cancellationToken;
 
 	private readonly InstanceLogSender logSender;
+	private readonly InstancePlayerCountTracker playerCountTracker;
 	private readonly BackupScheduler backupScheduler;
 
 	private bool isDisposed;
@@ -32,8 +33,9 @@ sealed class InstanceRunningState : IDisposable {
 		this.cancellationToken = cancellationToken;
 
 		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName);
+		this.playerCountTracker = new InstancePlayerCountTracker(context, process, configuration.ServerPort);
 
-		this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
+		this.backupScheduler = new BackupScheduler(context, playerCountTracker);
 		this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
 	}
 
@@ -93,6 +95,11 @@ sealed class InstanceRunningState : IDisposable {
 		}
 	}
 
+	public void OnStopInitiated() {
+		backupScheduler.Stop();
+		playerCountTracker.Stop();
+	}
+	
 	private bool TryDispose() {
 		lock (this) {
 			if (isDisposed) {
@@ -102,8 +109,8 @@ sealed class InstanceRunningState : IDisposable {
 			isDisposed = true;
 		}
 
+		OnStopInitiated();
 		logSender.Stop();
-		backupScheduler.Stop();
 		
 		Process.Dispose();
 		
diff --git a/Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs b/Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs
index 464a419..534f56b 100644
--- a/Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs
@@ -25,6 +25,8 @@ static class InstanceStopProcedure {
 
 		try {
 			// Too late to cancel the stop procedure now.
+			runningState.OnStopInitiated();
+			
 			if (!process.HasEnded) {
 				context.Logger.Information("Session stopping now.");
 				await DoStop(context, process);