diff --git a/Agent/Phantom.Agent.Minecraft/Instance/InstanceProcess.cs b/Agent/Phantom.Agent.Minecraft/Instance/InstanceProcess.cs
index 7efa7d3..ed8503e 100644
--- a/Agent/Phantom.Agent.Minecraft/Instance/InstanceProcess.cs
+++ b/Agent/Phantom.Agent.Minecraft/Instance/InstanceProcess.cs
@@ -1,5 +1,6 @@
 using Phantom.Utils.Collections;
 using Phantom.Utils.Processes;
+using Phantom.Utils.Tasks;
 
 namespace Phantom.Agent.Minecraft.Instance;
 
@@ -13,6 +14,7 @@ public sealed class InstanceProcess : IDisposable {
 	public bool HasEnded { get; private set; }
 
 	private readonly Process process;
+	private readonly TaskCompletionSource processExited = AsyncTasks.CreateCompletionSource();
 
 	internal InstanceProcess(InstanceProperties instanceProperties, Process process) {
 		this.InstanceProperties = instanceProperties;
@@ -46,16 +48,15 @@ public sealed class InstanceProcess : IDisposable {
 		OutputEvent = null;
 		HasEnded = true;
 		Ended?.Invoke(this, EventArgs.Empty);
+		processExited.SetResult();
 	}
 
 	public void Kill() {
 		process.Kill(true);
 	}
 
-	public async Task WaitForExit(CancellationToken cancellationToken) {
-		if (!HasEnded) {
-			await process.WaitForExitAsync(cancellationToken);
-		}
+	public async Task WaitForExit(TimeSpan timeout) {
+		await processExited.Task.WaitAsync(timeout);
 	}
 
 	public void Dispose() {
diff --git a/Agent/Phantom.Agent.Services/AgentServices.cs b/Agent/Phantom.Agent.Services/AgentServices.cs
index dedbac1..c31e118 100644
--- a/Agent/Phantom.Agent.Services/AgentServices.cs
+++ b/Agent/Phantom.Agent.Services/AgentServices.cs
@@ -1,8 +1,10 @@
-using Phantom.Agent.Minecraft.Java;
+using Akka.Actor;
+using Phantom.Agent.Minecraft.Java;
 using Phantom.Agent.Rpc;
 using Phantom.Agent.Services.Backups;
 using Phantom.Agent.Services.Instances;
 using Phantom.Common.Data.Agent;
+using Phantom.Utils.Actor;
 using Phantom.Utils.Logging;
 using Phantom.Utils.Tasks;
 using Serilog;
@@ -12,19 +14,30 @@ namespace Phantom.Agent.Services;
 public sealed class AgentServices {
 	private static readonly ILogger Logger = PhantomLogger.Create<AgentServices>();
 	
+	public ActorSystem ActorSystem { get; }
+
 	private AgentFolders AgentFolders { get; }
+	private AgentState AgentState { get; }
 	private TaskManager TaskManager { get; }
 	private BackupManager BackupManager { get; }
 
 	internal JavaRuntimeRepository JavaRuntimeRepository { get; }
-	internal InstanceSessionManager InstanceSessionManager { get; }
+	internal InstanceTicketManager InstanceTicketManager { get; }
+	internal ActorRef<InstanceManagerActor.ICommand> InstanceManager { get; }
 
 	public AgentServices(AgentInfo agentInfo, AgentFolders agentFolders, AgentServiceConfiguration serviceConfiguration, ControllerConnection controllerConnection) {
+		this.ActorSystem = ActorSystemFactory.Create("Agent");
+		
 		this.AgentFolders = agentFolders;
+		this.AgentState = new AgentState();
 		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
 		this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
+		
 		this.JavaRuntimeRepository = new JavaRuntimeRepository();
-		this.InstanceSessionManager = new InstanceSessionManager(controllerConnection, agentInfo, agentFolders, JavaRuntimeRepository, TaskManager, BackupManager);
+		this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
+		
+		var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, TaskManager, BackupManager);
+		this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
 	}
 
 	public async Task Initialize() {
@@ -36,11 +49,14 @@ public sealed class AgentServices {
 	public async Task Shutdown() {
 		Logger.Information("Stopping services...");
 		
-		await InstanceSessionManager.DisposeAsync();
+		await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
 		await TaskManager.Stop();
 		
 		BackupManager.Dispose();
 		
+		await ActorSystem.Terminate();
+		ActorSystem.Dispose();
+		
 		Logger.Information("Services stopped.");
 	}
 }
diff --git a/Agent/Phantom.Agent.Services/AgentState.cs b/Agent/Phantom.Agent.Services/AgentState.cs
new file mode 100644
index 0000000..1e251a2
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/AgentState.cs
@@ -0,0 +1,15 @@
+using System.Collections.Immutable;
+using Phantom.Agent.Services.Instances;
+using Phantom.Utils.Actor.Event;
+
+namespace Phantom.Agent.Services;
+
+sealed class AgentState {
+	private readonly ObservableState<ImmutableDictionary<Guid, Instance>> instancesByGuid = new (ImmutableDictionary<Guid, Instance>.Empty);
+	
+	public ImmutableDictionary<Guid, Instance> InstancesByGuid => instancesByGuid.State;
+	
+	public void UpdateInstance(Instance instance) {
+		instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance);
+	}
+}
diff --git a/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs b/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
index 31fa578..3853625 100644
--- a/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
+++ b/Agent/Phantom.Agent.Services/Backups/BackupScheduler.cs
@@ -1,7 +1,6 @@
 using Phantom.Agent.Minecraft.Instance;
 using Phantom.Agent.Minecraft.Server;
 using Phantom.Agent.Services.Instances;
-using Phantom.Agent.Services.Instances.Procedures;
 using Phantom.Common.Data.Backups;
 using Phantom.Utils.Logging;
 using Phantom.Utils.Tasks;
@@ -16,8 +15,8 @@ sealed class BackupScheduler : CancellableBackgroundTask {
 	private static readonly TimeSpan BackupFailureRetryDelay = TimeSpan.FromMinutes(5);
 
 	private readonly BackupManager backupManager;
+	private readonly InstanceContext context;
 	private readonly InstanceProcess process;
-	private readonly IInstanceContext context;
 	private readonly SemaphoreSlim backupSemaphore = new (1, 1);
 	private readonly int serverPort;
 	private readonly ServerStatusProtocol serverStatusProtocol;
@@ -25,10 +24,10 @@ sealed class BackupScheduler : CancellableBackgroundTask {
 	
 	public event EventHandler<BackupCreationResult>? BackupCompleted;
 
-	public BackupScheduler(TaskManager taskManager, BackupManager backupManager, InstanceProcess process, IInstanceContext context, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), taskManager, "Backup scheduler for " + context.ShortName) {
-		this.backupManager = backupManager;
-		this.process = process;
+	public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), context.Services.TaskManager, "Backup scheduler for " + context.ShortName) {
+		this.backupManager = context.Services.BackupManager;
 		this.context = context;
+		this.process = process;
 		this.serverPort = serverPort;
 		this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
 		Start();
@@ -60,9 +59,10 @@ sealed class BackupScheduler : CancellableBackgroundTask {
 		}
 		
 		try {
-			var procedure = new BackupInstanceProcedure(backupManager);
-			context.EnqueueProcedure(procedure);
-			return await procedure.Result;
+			context.ActorCancellationToken.ThrowIfCancellationRequested();
+			return await context.Actor.Request(new InstanceActor.BackupInstanceCommand(backupManager), context.ActorCancellationToken);
+		} catch (OperationCanceledException) {
+			return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
 		} finally {
 			backupSemaphore.Release();
 		}
diff --git a/Agent/Phantom.Agent.Services/Instances/IInstanceContext.cs b/Agent/Phantom.Agent.Services/Instances/IInstanceContext.cs
deleted file mode 100644
index dd72b2f..0000000
--- a/Agent/Phantom.Agent.Services/Instances/IInstanceContext.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Phantom.Agent.Services.Instances.Procedures;
-using Phantom.Agent.Services.Instances.States;
-using Phantom.Common.Data.Instance;
-using Serilog;
-
-namespace Phantom.Agent.Services.Instances;
-
-interface IInstanceContext {
-	string ShortName { get; }
-	ILogger Logger { get; }
-	
-	InstanceServices Services { get; }
-	IInstanceState CurrentState { get; }
-
-	void SetStatus(IInstanceStatus newStatus);
-	void ReportEvent(IInstanceEvent instanceEvent);
-	void EnqueueProcedure(IInstanceProcedure procedure, bool immediate = false);
-}
-
-static class InstanceContextExtensions {
-	public static void SetLaunchFailedStatusAndReportEvent(this IInstanceContext context, InstanceLaunchFailReason reason) {
-		context.SetStatus(InstanceStatus.Failed(reason));
-		context.ReportEvent(new InstanceLaunchFailedEvent(reason));
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/Instance.cs b/Agent/Phantom.Agent.Services/Instances/Instance.cs
index a8abd07..d298cdd 100644
--- a/Agent/Phantom.Agent.Services/Instances/Instance.cs
+++ b/Agent/Phantom.Agent.Services/Instances/Instance.cs
@@ -1,171 +1,5 @@
-using Phantom.Agent.Minecraft.Launcher;
-using Phantom.Agent.Services.Instances.Procedures;
-using Phantom.Agent.Services.Instances.States;
-using Phantom.Common.Data.Instance;
-using Phantom.Common.Data.Minecraft;
-using Phantom.Common.Data.Replies;
-using Phantom.Common.Messages.Agent.ToController;
-using Phantom.Utils.Logging;
-using Serilog;
+using Phantom.Common.Data.Instance;
 
 namespace Phantom.Agent.Services.Instances;
 
-sealed class Instance : IAsyncDisposable {
-	private InstanceServices Services { get; }
-	
-	public InstanceConfiguration Configuration { get; private set; }
-	private IServerLauncher Launcher { get; set; }
-	private readonly SemaphoreSlim configurationSemaphore = new (1, 1);
-
-	private readonly Guid instanceGuid;
-	private readonly string shortName;
-	private readonly ILogger logger;
-
-	private IInstanceStatus currentStatus;
-
-	private IInstanceState currentState;
-	public bool IsRunning => currentState is not InstanceNotRunningState;
-	
-	public event EventHandler? IsRunningChanged;
-	
-	private readonly InstanceProcedureManager procedureManager;
-
-	public Instance(Guid instanceGuid, string shortName, InstanceServices services, InstanceConfiguration configuration, IServerLauncher launcher) {
-		this.instanceGuid = instanceGuid;
-		this.shortName = shortName;
-		this.logger = PhantomLogger.Create<Instance>(shortName);
-
-		this.Services = services;
-		this.Configuration = configuration;
-		this.Launcher = launcher;
-		
-		this.currentStatus = InstanceStatus.NotRunning;
-		this.currentState = new InstanceNotRunningState();
-		
-		this.procedureManager = new InstanceProcedureManager(this, new Context(this), services.TaskManager);
-	}
-
-	public void ReportLastStatus() {
-		Services.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
-	}
-
-	private void ReportAndSetStatus(IInstanceStatus status) {
-		currentStatus = status;
-		Services.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, status));
-	}
-
-	private void ReportEvent(IInstanceEvent instanceEvent) {
-		Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, instanceGuid, instanceEvent));
-	}
-	
-	internal void TransitionState(IInstanceState newState) {
-		if (currentState == newState) {
-			return;
-		}
-
-		if (currentState is IDisposable disposable) {
-			disposable.Dispose();
-		}
-
-		logger.Debug("Transitioning instance state to: {NewState}", newState.GetType().Name);
-		
-		var wasRunning = IsRunning;
-		currentState = newState;
-		currentState.Initialize();
-
-		if (IsRunning != wasRunning) {
-			IsRunningChanged?.Invoke(this, EventArgs.Empty);
-		}
-	}
-
-	public async Task Reconfigure(InstanceConfiguration configuration, IServerLauncher launcher, CancellationToken cancellationToken) {
-		await configurationSemaphore.WaitAsync(cancellationToken);
-		try {
-			Configuration = configuration;
-			Launcher = launcher;
-		} finally {
-			configurationSemaphore.Release();
-		}
-	}
-
-	public async Task<LaunchInstanceResult> Launch(CancellationToken cancellationToken) {
-		if (IsRunning) {
-			return LaunchInstanceResult.InstanceAlreadyRunning;
-		}
-
-		if (await procedureManager.GetCurrentProcedure(cancellationToken) is LaunchInstanceProcedure) {
-			return LaunchInstanceResult.InstanceAlreadyLaunching;
-		}
-		
-		LaunchInstanceProcedure procedure;
-		
-		await configurationSemaphore.WaitAsync(cancellationToken);
-		try {
-			procedure = new LaunchInstanceProcedure(instanceGuid, Configuration, Launcher);
-		} finally {
-			configurationSemaphore.Release();
-		}
-		
-		ReportAndSetStatus(InstanceStatus.Launching);
-		await procedureManager.Enqueue(procedure);
-		return LaunchInstanceResult.LaunchInitiated;
-	}
-
-	public async Task<StopInstanceResult> Stop(MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
-		if (!IsRunning) {
-			return StopInstanceResult.InstanceAlreadyStopped;
-		}
-		
-		if (await procedureManager.GetCurrentProcedure(cancellationToken) is StopInstanceProcedure) {
-			return StopInstanceResult.InstanceAlreadyStopping;
-		}
-		
-		ReportAndSetStatus(InstanceStatus.Stopping);
-		await procedureManager.Enqueue(new StopInstanceProcedure(stopStrategy));
-		return StopInstanceResult.StopInitiated;
-	}
-
-	public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
-		return await currentState.SendCommand(command, cancellationToken);
-	}
-
-	public async ValueTask DisposeAsync() {
-		await procedureManager.DisposeAsync();
-
-		while (currentState is not InstanceNotRunningState) {
-			await Task.Delay(TimeSpan.FromMilliseconds(250), CancellationToken.None);
-		}
-
-		if (currentState is IDisposable disposable) {
-			disposable.Dispose();
-		}
-		
-		configurationSemaphore.Dispose();
-	}
-
-	private sealed class Context : IInstanceContext {
-		public string ShortName => instance.shortName;
-		public ILogger Logger => instance.logger;
-		
-		public InstanceServices Services => instance.Services;
-		public IInstanceState CurrentState => instance.currentState;
-
-		private readonly Instance instance;
-		
-		public Context(Instance instance) {
-			this.instance = instance;
-		}
-
-		public void SetStatus(IInstanceStatus newStatus) {
-			instance.ReportAndSetStatus(newStatus);
-		}
-
-		public void ReportEvent(IInstanceEvent instanceEvent) {
-			instance.ReportEvent(instanceEvent);
-		}
-
-		public void EnqueueProcedure(IInstanceProcedure procedure, bool immediate) {
-			Services.TaskManager.Run("Enqueue procedure for instance " + instance.shortName, () => instance.procedureManager.Enqueue(procedure, immediate));
-		}
-	}
-}
+sealed record Instance(Guid InstanceGuid, IInstanceStatus Status);
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceActor.cs b/Agent/Phantom.Agent.Services/Instances/InstanceActor.cs
new file mode 100644
index 0000000..908a8b5
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/Instances/InstanceActor.cs
@@ -0,0 +1,156 @@
+using Phantom.Agent.Minecraft.Launcher;
+using Phantom.Agent.Services.Backups;
+using Phantom.Agent.Services.Instances.State;
+using Phantom.Common.Data.Backups;
+using Phantom.Common.Data.Instance;
+using Phantom.Common.Data.Minecraft;
+using Phantom.Common.Data.Replies;
+using Phantom.Common.Messages.Agent.ToController;
+using Phantom.Utils.Actor;
+using Phantom.Utils.Actor.Mailbox;
+using Phantom.Utils.Logging;
+
+namespace Phantom.Agent.Services.Instances;
+
+sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
+	public readonly record struct Init(AgentState AgentState, Guid InstanceGuid, string ShortName, InstanceServices InstanceServices, InstanceTicketManager InstanceTicketManager, CancellationToken ShutdownCancellationToken);
+
+	public static Props<ICommand> Factory(Init init) {
+		return Props<ICommand>.Create(() => new InstanceActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name });
+	}
+
+	private readonly AgentState agentState;
+	private readonly CancellationToken shutdownCancellationToken;
+	
+	private readonly Guid instanceGuid;
+	private readonly InstanceServices instanceServices;
+	private readonly InstanceTicketManager instanceTicketManager;
+	private readonly InstanceContext context;
+	
+	private readonly CancellationTokenSource actorCancellationTokenSource = new ();
+
+	private IInstanceStatus currentStatus = InstanceStatus.NotRunning;
+	private InstanceRunningState? runningState = null;
+
+	private InstanceActor(Init init) {
+		this.agentState = init.AgentState;
+		this.instanceGuid = init.InstanceGuid;
+		this.instanceServices = init.InstanceServices;
+		this.instanceTicketManager = init.InstanceTicketManager;
+		this.shutdownCancellationToken = init.ShutdownCancellationToken;
+		
+		var logger = PhantomLogger.Create<InstanceActor>(init.ShortName);
+		this.context = new InstanceContext(instanceGuid, init.ShortName, logger, instanceServices, SelfTyped, actorCancellationTokenSource.Token);
+
+		Receive<ReportInstanceStatusCommand>(ReportInstanceStatus);
+		ReceiveAsync<LaunchInstanceCommand>(LaunchInstance);
+		ReceiveAsync<StopInstanceCommand>(StopInstance);
+		ReceiveAsyncAndReply<SendCommandToInstanceCommand, SendCommandToInstanceResult>(SendCommandToInstance);
+		ReceiveAsyncAndReply<BackupInstanceCommand, BackupCreationResult>(BackupInstance);
+		Receive<HandleProcessEndedCommand>(HandleProcessEnded);
+		ReceiveAsync<ShutdownCommand>(Shutdown);
+	}
+
+	private void SetAndReportStatus(IInstanceStatus status) {
+		currentStatus = status;
+		ReportCurrentStatus();
+	}
+
+	private void ReportCurrentStatus() {
+		agentState.UpdateInstance(new Instance(instanceGuid, currentStatus));
+		instanceServices.ControllerConnection.Send(new ReportInstanceStatusMessage(instanceGuid, currentStatus));
+	}
+
+	private void TransitionState(InstanceRunningState? newState) {
+		if (runningState == newState) {
+			return;
+		}
+
+		runningState?.Dispose();
+		runningState = newState;
+		runningState?.Initialize();
+	}
+	
+	public interface ICommand {}
+
+	public sealed record ReportInstanceStatusCommand : ICommand;
+	
+	public sealed record LaunchInstanceCommand(InstanceConfiguration Configuration, IServerLauncher Launcher, InstanceTicketManager.Ticket Ticket, bool IsRestarting) : ICommand;
+	
+	public sealed record StopInstanceCommand(MinecraftStopStrategy StopStrategy) : ICommand;
+	
+	public sealed record SendCommandToInstanceCommand(string Command) : ICommand, ICanReply<SendCommandToInstanceResult>;
+	
+	public sealed record BackupInstanceCommand(BackupManager BackupManager) : ICommand, ICanReply<BackupCreationResult>;
+	
+	public sealed record HandleProcessEndedCommand(IInstanceStatus Status) : ICommand, IJumpAhead;
+	
+	public sealed record ShutdownCommand : ICommand;
+
+	private void ReportInstanceStatus(ReportInstanceStatusCommand command) {
+		ReportCurrentStatus();
+	}
+	
+	private async Task LaunchInstance(LaunchInstanceCommand command) {
+		if (command.IsRestarting || runningState is null) {
+			SetAndReportStatus(command.IsRestarting ? InstanceStatus.Restarting : InstanceStatus.Launching);
+			
+			var newState = await InstanceLaunchProcedure.Run(context, command.Configuration, command.Launcher, instanceTicketManager, command.Ticket, SetAndReportStatus, shutdownCancellationToken);
+			if (newState is null) {
+				instanceTicketManager.Release(command.Ticket);
+			}
+			
+			TransitionState(newState);
+		}
+	}
+
+	private async Task StopInstance(StopInstanceCommand command) {
+		if (runningState is null) {
+			return;
+		}
+
+		IInstanceStatus oldStatus = currentStatus;
+		SetAndReportStatus(InstanceStatus.Stopping);
+		
+		if (await InstanceStopProcedure.Run(context, command.StopStrategy, runningState, SetAndReportStatus, shutdownCancellationToken)) {
+			instanceTicketManager.Release(runningState.Ticket);
+			TransitionState(null);
+		}
+		else {
+			SetAndReportStatus(oldStatus);
+		}
+	}
+
+	private async Task<SendCommandToInstanceResult> SendCommandToInstance(SendCommandToInstanceCommand command) {
+		if (runningState is null) {
+			return SendCommandToInstanceResult.InstanceNotRunning;
+		}
+		else {
+			return await runningState.SendCommand(command.Command, shutdownCancellationToken);
+		}
+	}
+	
+	private async Task<BackupCreationResult> BackupInstance(BackupInstanceCommand command) {
+		if (runningState is null || runningState.Process.HasEnded) {
+			return new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning);
+		}
+		else {
+			return await command.BackupManager.CreateBackup(context.ShortName, runningState.Process, shutdownCancellationToken);
+		}
+	}
+
+	private void HandleProcessEnded(HandleProcessEndedCommand command) {
+		if (runningState is { Process.HasEnded: true }) {
+			SetAndReportStatus(command.Status);
+			context.ReportEvent(InstanceEvent.Stopped);
+			instanceTicketManager.Release(runningState.Ticket);
+			TransitionState(null);
+		}
+	}
+
+	private async Task Shutdown(ShutdownCommand command) {
+		await StopInstance(new StopInstanceCommand(MinecraftStopStrategy.Instant));
+		await actorCancellationTokenSource.CancelAsync();
+		Context.Stop(Self);
+	}
+}
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceContext.cs b/Agent/Phantom.Agent.Services/Instances/InstanceContext.cs
new file mode 100644
index 0000000..25b5d8a
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/Instances/InstanceContext.cs
@@ -0,0 +1,12 @@
+using Phantom.Common.Data.Instance;
+using Phantom.Common.Messages.Agent.ToController;
+using Phantom.Utils.Actor;
+using Serilog;
+
+namespace Phantom.Agent.Services.Instances;
+
+sealed record InstanceContext(Guid InstanceGuid, string ShortName, ILogger Logger, InstanceServices Services, ActorRef<InstanceActor.ICommand> Actor, CancellationToken ActorCancellationToken) {
+	public void ReportEvent(IInstanceEvent instanceEvent) {
+		Services.ControllerConnection.Send(new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, InstanceGuid, instanceEvent));
+	}
+}
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceManagerActor.cs b/Agent/Phantom.Agent.Services/Instances/InstanceManagerActor.cs
new file mode 100644
index 0000000..f036b51
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/Instances/InstanceManagerActor.cs
@@ -0,0 +1,208 @@
+using Phantom.Agent.Minecraft.Instance;
+using Phantom.Agent.Minecraft.Java;
+using Phantom.Agent.Minecraft.Launcher;
+using Phantom.Agent.Minecraft.Launcher.Types;
+using Phantom.Agent.Minecraft.Properties;
+using Phantom.Agent.Minecraft.Server;
+using Phantom.Agent.Rpc;
+using Phantom.Agent.Services.Backups;
+using Phantom.Common.Data.Instance;
+using Phantom.Common.Data.Minecraft;
+using Phantom.Common.Data.Replies;
+using Phantom.Utils.Actor;
+using Phantom.Utils.IO;
+using Phantom.Utils.Logging;
+using Phantom.Utils.Tasks;
+using Serilog;
+
+namespace Phantom.Agent.Services.Instances;
+
+sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
+	private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>();
+
+	public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, TaskManager TaskManager, BackupManager BackupManager);
+
+	public static Props<ICommand> Factory(Init init) {
+		return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
+	}
+	
+	private readonly AgentState agentState;
+	private readonly string basePath;
+
+	private readonly InstanceServices instanceServices;
+	private readonly InstanceTicketManager instanceTicketManager;
+	private readonly Dictionary<Guid, InstanceInfo> instances = new ();
+
+	private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
+	private readonly CancellationToken shutdownCancellationToken;
+
+	private uint instanceLoggerSequenceId = 0;
+
+	private InstanceManagerActor(Init init) {
+		this.agentState = init.AgentState;
+		this.basePath = init.AgentFolders.InstancesFolderPath;
+		this.instanceTicketManager = init.InstanceTicketManager;
+		this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
+
+		var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath);
+		var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository);
+
+		this.instanceServices = new InstanceServices(init.ControllerConnection, init.TaskManager, init.BackupManager, launchServices);
+		
+		ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance);
+		ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
+		ReceiveAndReply<StopInstanceCommand, InstanceActionResult<StopInstanceResult>>(StopInstance);
+		ReceiveAsyncAndReply<SendCommandToInstanceCommand, InstanceActionResult<SendCommandToInstanceResult>>(SendCommandToInstance);
+		ReceiveAsync<ShutdownCommand>(Shutdown);
+	}
+
+	private string GetInstanceLoggerName(Guid guid) {
+		var prefix = guid.ToString();
+		return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
+	}
+
+	private sealed record InstanceInfo(ActorRef<InstanceActor.ICommand> Actor, InstanceConfiguration Configuration, IServerLauncher Launcher);
+	
+	public interface ICommand {}
+	
+	public sealed record ConfigureInstanceCommand(Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool LaunchNow, bool AlwaysReportStatus) : ICommand, ICanReply<InstanceActionResult<ConfigureInstanceResult>>;
+	
+	public sealed record LaunchInstanceCommand(Guid InstanceGuid) : ICommand, ICanReply<InstanceActionResult<LaunchInstanceResult>>;
+	
+	public sealed record StopInstanceCommand(Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<InstanceActionResult<StopInstanceResult>>;
+	
+	public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, string Command) : ICommand, ICanReply<InstanceActionResult<SendCommandToInstanceResult>>;
+	
+	public sealed record ShutdownCommand : ICommand;
+
+	private InstanceActionResult<ConfigureInstanceResult> ConfigureInstance(ConfigureInstanceCommand command) {
+		var instanceGuid = command.InstanceGuid;
+		var configuration = command.Configuration;
+
+		var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
+		Directories.Create(instanceFolder, Chmod.URWX_GRX);
+
+		var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
+		var jvmProperties = new JvmProperties(
+			InitialHeapMegabytes: heapMegabytes / 2,
+			MaximumHeapMegabytes: heapMegabytes
+		);
+
+		var properties = new InstanceProperties(
+			instanceGuid,
+			configuration.JavaRuntimeGuid,
+			jvmProperties,
+			configuration.JvmArguments,
+			instanceFolder,
+			configuration.MinecraftVersion,
+			new ServerProperties(configuration.ServerPort, configuration.RconPort),
+			command.LaunchProperties
+		);
+
+		IServerLauncher launcher = configuration.MinecraftServerKind switch {
+			MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
+			MinecraftServerKind.Fabric  => new FabricLauncher(properties),
+			_                           => InvalidLauncher.Instance
+		};
+
+		if (instances.TryGetValue(instanceGuid, out var instance)) {
+			instances[instanceGuid] = instance with {
+				Configuration = configuration,
+				Launcher = launcher
+			};
+				
+			Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
+
+			if (command.AlwaysReportStatus) {
+				instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
+			}
+		}
+		else {
+			var instanceInit = new InstanceActor.Init(agentState, instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, instanceTicketManager, shutdownCancellationToken);
+			instances[instanceGuid] = instance = new InstanceInfo(Context.ActorOf(InstanceActor.Factory(instanceInit), "Instance-" + instanceGuid), configuration, launcher);
+				
+			Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
+
+			instance.Actor.Tell(new InstanceActor.ReportInstanceStatusCommand());
+		}
+
+		if (command.LaunchNow) {
+			LaunchInstance(new LaunchInstanceCommand(instanceGuid));
+		}
+
+		return InstanceActionResult.Concrete(ConfigureInstanceResult.Success);
+	}
+
+	private InstanceActionResult<LaunchInstanceResult> LaunchInstance(LaunchInstanceCommand command) {
+		var instanceGuid = command.InstanceGuid;
+		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
+			return InstanceActionResult.General<LaunchInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
+		}
+		
+		var ticket = instanceTicketManager.Reserve(instanceInfo.Configuration);
+		if (ticket is Result<InstanceTicketManager.Ticket, LaunchInstanceResult>.Fail fail) {
+			return InstanceActionResult.Concrete(fail.Error);
+		}
+
+		if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
+			var status = instance.Status;
+			if (status.IsRunning()) {
+				return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyRunning);
+			}
+			else if (status.IsLaunching()) {
+				return InstanceActionResult.Concrete(LaunchInstanceResult.InstanceAlreadyLaunching);
+			}
+		}
+		
+		instanceInfo.Actor.Tell(new InstanceActor.LaunchInstanceCommand(instanceInfo.Configuration, instanceInfo.Launcher, ticket.Value, IsRestarting: false));
+		return InstanceActionResult.Concrete(LaunchInstanceResult.LaunchInitiated);
+	}
+
+	private InstanceActionResult<StopInstanceResult> StopInstance(StopInstanceCommand command) {
+		var instanceGuid = command.InstanceGuid;
+		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
+			return InstanceActionResult.General<StopInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
+		}
+        
+		if (agentState.InstancesByGuid.TryGetValue(instanceGuid, out var instance)) {
+			var status = instance.Status;
+			if (status.IsStopping()) {
+				return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopping);
+			}
+			else if (!status.CanStop()) {
+				return InstanceActionResult.Concrete(StopInstanceResult.InstanceAlreadyStopped);
+			}
+		}
+			
+		instanceInfo.Actor.Tell(new InstanceActor.StopInstanceCommand(command.StopStrategy));
+		return InstanceActionResult.Concrete(StopInstanceResult.StopInitiated);
+	}
+
+	private async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommandToInstance(SendCommandToInstanceCommand command) {
+		var instanceGuid = command.InstanceGuid;
+		if (!instances.TryGetValue(instanceGuid, out var instanceInfo)) {
+			return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.InstanceDoesNotExist);
+		}
+
+		try {
+			return InstanceActionResult.Concrete(await instanceInfo.Actor.Request(new InstanceActor.SendCommandToInstanceCommand(command.Command), shutdownCancellationToken));
+		} catch (OperationCanceledException) {
+			return InstanceActionResult.General<SendCommandToInstanceResult>(InstanceActionGeneralResult.AgentShuttingDown);
+		}
+	}
+
+	private async Task Shutdown(ShutdownCommand command) {
+		Logger.Information("Stopping all instances...");
+		
+		await shutdownCancellationTokenSource.CancelAsync();
+		
+		await Task.WhenAll(instances.Values.Select(static instance => instance.Actor.Stop(new InstanceActor.ShutdownCommand())));
+		instances.Clear();
+		
+		shutdownCancellationTokenSource.Dispose();
+		
+		Logger.Information("All instances stopped.");
+		
+		Context.Stop(Self);
+	}
+}
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceProcedureManager.cs b/Agent/Phantom.Agent.Services/Instances/InstanceProcedureManager.cs
deleted file mode 100644
index 695340e..0000000
--- a/Agent/Phantom.Agent.Services/Instances/InstanceProcedureManager.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using Phantom.Agent.Services.Instances.Procedures;
-using Phantom.Common.Data.Minecraft;
-using Phantom.Utils.Tasks;
-using Phantom.Utils.Threading;
-
-namespace Phantom.Agent.Services.Instances;
-
-sealed class InstanceProcedureManager : IAsyncDisposable {
-	private readonly record struct CurrentProcedure(IInstanceProcedure Procedure, CancellationTokenSource CancellationTokenSource);
-
-	private readonly ThreadSafeStructRef<CurrentProcedure> currentProcedure = new ();
-	private readonly ThreadSafeLinkedList<IInstanceProcedure> procedureQueue = new ();
-	private readonly AutoResetEvent procedureQueueReady = new (false);
-	private readonly ManualResetEventSlim procedureQueueFinished = new (false);
-
-	private readonly Instance instance;
-	private readonly IInstanceContext context;
-	private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
-
-	public InstanceProcedureManager(Instance instance, IInstanceContext context, TaskManager taskManager) {
-		this.instance = instance;
-		this.context = context;
-		taskManager.Run("Procedure manager for instance " + context.ShortName, Run);
-	}
-
-	public async Task Enqueue(IInstanceProcedure procedure, bool immediate = false) {
-		await procedureQueue.Add(procedure, toFront: immediate, shutdownCancellationTokenSource.Token);
-		procedureQueueReady.Set();
-	}
-
-	public async Task<IInstanceProcedure?> GetCurrentProcedure(CancellationToken cancellationToken) {
-		return (await currentProcedure.Get(cancellationToken))?.Procedure;
-	}
-
-	private async Task Run() {
-		try {
-			var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
-			while (true) {
-				await procedureQueueReady.WaitOneAsync(shutdownCancellationToken);
-				while (await procedureQueue.TryTakeFromFront(shutdownCancellationToken) is {} nextProcedure) {
-					using var procedureCancellationTokenSource = new CancellationTokenSource();
-					await currentProcedure.Set(new CurrentProcedure(nextProcedure, procedureCancellationTokenSource), shutdownCancellationToken);
-					await RunProcedure(nextProcedure, procedureCancellationTokenSource.Token);
-					await currentProcedure.Set(null, shutdownCancellationToken);
-				}
-			}
-		} catch (OperationCanceledException) {
-			// Ignore.
-		}
-
-		await RunProcedure(new StopInstanceProcedure(MinecraftStopStrategy.Instant), CancellationToken.None);
-		procedureQueueFinished.Set();
-	}
-
-	private async Task RunProcedure(IInstanceProcedure procedure, CancellationToken cancellationToken) {
-		var procedureName = procedure.GetType().Name;
-
-		context.Logger.Debug("Started procedure: {Procedure}", procedureName);
-		try {
-			var newState = await procedure.Run(context, cancellationToken);
-			context.Logger.Debug("Finished procedure: {Procedure}", procedureName);
-
-			if (newState != null) {
-				instance.TransitionState(newState);
-			}
-		} catch (OperationCanceledException) {
-			context.Logger.Debug("Cancelled procedure: {Procedure}", procedureName);
-		} catch (Exception e) {
-			context.Logger.Error(e, "Caught exception while running procedure: {Procedure}", procedureName);
-		}
-	}
-
-	public async ValueTask DisposeAsync() {
-		shutdownCancellationTokenSource.Cancel();
-
-		(await currentProcedure.Get(CancellationToken.None))?.CancellationTokenSource.Cancel();
-		await procedureQueueFinished.WaitHandle.WaitOneAsync();
-
-		currentProcedure.Dispose();
-		procedureQueue.Dispose();
-		procedureQueueReady.Dispose();
-		procedureQueueFinished.Dispose();
-		shutdownCancellationTokenSource.Dispose();
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceServices.cs b/Agent/Phantom.Agent.Services/Instances/InstanceServices.cs
index c2d3f5c..b8363ba 100644
--- a/Agent/Phantom.Agent.Services/Instances/InstanceServices.cs
+++ b/Agent/Phantom.Agent.Services/Instances/InstanceServices.cs
@@ -5,4 +5,4 @@ using Phantom.Utils.Tasks;
 
 namespace Phantom.Agent.Services.Instances;
 
-sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, PortManager PortManager, BackupManager BackupManager, LaunchServices LaunchServices);
+sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, BackupManager BackupManager, LaunchServices LaunchServices);
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceSessionManager.cs b/Agent/Phantom.Agent.Services/Instances/InstanceSessionManager.cs
deleted file mode 100644
index 2289691..0000000
--- a/Agent/Phantom.Agent.Services/Instances/InstanceSessionManager.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using System.Collections.Immutable;
-using System.Diagnostics.CodeAnalysis;
-using Phantom.Agent.Minecraft.Instance;
-using Phantom.Agent.Minecraft.Java;
-using Phantom.Agent.Minecraft.Launcher;
-using Phantom.Agent.Minecraft.Launcher.Types;
-using Phantom.Agent.Minecraft.Properties;
-using Phantom.Agent.Minecraft.Server;
-using Phantom.Agent.Rpc;
-using Phantom.Agent.Services.Backups;
-using Phantom.Common.Data;
-using Phantom.Common.Data.Agent;
-using Phantom.Common.Data.Instance;
-using Phantom.Common.Data.Minecraft;
-using Phantom.Common.Data.Replies;
-using Phantom.Common.Messages.Agent.ToController;
-using Phantom.Utils.IO;
-using Phantom.Utils.Logging;
-using Phantom.Utils.Tasks;
-using Serilog;
-
-namespace Phantom.Agent.Services.Instances;
-
-sealed class InstanceSessionManager : IAsyncDisposable {
-	private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>();
-
-	private readonly ControllerConnection controllerConnection;
-	private readonly AgentInfo agentInfo;
-	private readonly string basePath;
-
-	private readonly InstanceServices instanceServices;
-	private readonly Dictionary<Guid, Instance> instances = new ();
-
-	private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
-	private readonly CancellationToken shutdownCancellationToken;
-	private readonly SemaphoreSlim semaphore = new (1, 1);
-
-	private uint instanceLoggerSequenceId = 0;
-
-	public InstanceSessionManager(ControllerConnection controllerConnection, AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
-		this.controllerConnection = controllerConnection;
-		this.agentInfo = agentInfo;
-		this.basePath = agentFolders.InstancesFolderPath;
-		this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;
-
-		var minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
-		var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
-		var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
-
-		this.instanceServices = new InstanceServices(controllerConnection, taskManager, portManager, backupManager, launchServices);
-	}
-
-	private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) {
-		try {
-			await semaphore.WaitAsync(shutdownCancellationToken);
-			try {
-				return await func();
-			} finally {
-				semaphore.Release();
-			}
-		} catch (OperationCanceledException) {
-			return InstanceActionResult.General<T>(InstanceActionGeneralResult.AgentShuttingDown);
-		}
-	}
-
-	[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
-	private Task<InstanceActionResult<T>> AcquireSemaphoreAndRunWithInstance<T>(Guid instanceGuid, Func<Instance, Task<T>> func) {
-		return AcquireSemaphoreAndRun(async () => {
-			if (instances.TryGetValue(instanceGuid, out var instance)) {
-				return InstanceActionResult.Concrete(await func(instance));
-			}
-			else {
-				return InstanceActionResult.General<T>(InstanceActionGeneralResult.InstanceDoesNotExist);
-			}
-		});
-	}
-
-	public async Task<InstanceActionResult<ConfigureInstanceResult>> Configure(Guid instanceGuid, InstanceConfiguration configuration, InstanceLaunchProperties launchProperties, bool launchNow, bool alwaysReportStatus) {
-		return await AcquireSemaphoreAndRun(async () => {
-			var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
-			Directories.Create(instanceFolder, Chmod.URWX_GRX);
-
-			var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
-			var jvmProperties = new JvmProperties(
-				InitialHeapMegabytes: heapMegabytes / 2,
-				MaximumHeapMegabytes: heapMegabytes
-			);
-
-			var properties = new InstanceProperties(
-				instanceGuid,
-				configuration.JavaRuntimeGuid,
-				jvmProperties,
-				configuration.JvmArguments,
-				instanceFolder,
-				configuration.MinecraftVersion,
-				new ServerProperties(configuration.ServerPort, configuration.RconPort),
-				launchProperties
-			);
-
-			IServerLauncher launcher = configuration.MinecraftServerKind switch {
-				MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
-				MinecraftServerKind.Fabric  => new FabricLauncher(properties),
-				_                           => InvalidLauncher.Instance
-			};
-
-			if (instances.TryGetValue(instanceGuid, out var instance)) {
-				await instance.Reconfigure(configuration, launcher, shutdownCancellationToken);
-				Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
-
-				if (alwaysReportStatus) {
-					instance.ReportLastStatus();
-				}
-			}
-			else {
-				instances[instanceGuid] = instance = new Instance(instanceGuid, GetInstanceLoggerName(instanceGuid), instanceServices, configuration, launcher);
-				Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, instanceGuid);
-
-				instance.ReportLastStatus();
-				instance.IsRunningChanged += OnInstanceIsRunningChanged;
-			}
-
-			if (launchNow) {
-				await LaunchInternal(instance);
-			}
-
-			return InstanceActionResult.Concrete(ConfigureInstanceResult.Success);
-		});
-	}
-
-	private string GetInstanceLoggerName(Guid guid) {
-		var prefix = guid.ToString();
-		return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
-	}
-
-	private ImmutableArray<Instance> GetRunningInstancesInternal() {
-		return instances.Values.Where(static instance => instance.IsRunning).ToImmutableArray();
-	}
-
-	private void OnInstanceIsRunningChanged(object? sender, EventArgs e) {
-		instanceServices.TaskManager.Run("Handle instance running state changed event", RefreshAgentStatus);
-	}
-
-	public async Task RefreshAgentStatus() {
-		try {
-			await semaphore.WaitAsync(shutdownCancellationToken);
-			try {
-				var runningInstances = GetRunningInstancesInternal();
-				var runningInstanceCount = runningInstances.Length;
-				var runningInstanceMemory = runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation);
-				await controllerConnection.Send(new ReportAgentStatusMessage(runningInstanceCount, runningInstanceMemory));
-			} finally {
-				semaphore.Release();
-			}
-		} catch (OperationCanceledException) {
-			// ignore
-		}
-	}
-
-	public Task<InstanceActionResult<LaunchInstanceResult>> Launch(Guid instanceGuid) {
-		return AcquireSemaphoreAndRunWithInstance(instanceGuid, LaunchInternal);
-	}
-
-	private async Task<LaunchInstanceResult> LaunchInternal(Instance instance) {
-		var runningInstances = GetRunningInstancesInternal();
-		if (runningInstances.Length + 1 > agentInfo.MaxInstances) {
-			return LaunchInstanceResult.InstanceLimitExceeded;
-		}
-
-		var availableMemory = agentInfo.MaxMemory - runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation);
-		if (availableMemory < instance.Configuration.MemoryAllocation) {
-			return LaunchInstanceResult.MemoryLimitExceeded;
-		}
-
-		return await instance.Launch(shutdownCancellationToken);
-	}
-
-	public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
-		return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy, shutdownCancellationToken));
-	}
-
-	public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
-		return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError);
-	}
-
-	public async ValueTask DisposeAsync() {
-		Logger.Information("Stopping all instances...");
-		
-		shutdownCancellationTokenSource.Cancel();
-
-		await semaphore.WaitAsync(CancellationToken.None);
-		await Task.WhenAll(instances.Values.Select(static instance => instance.DisposeAsync().AsTask()));
-		instances.Clear();
-		
-		shutdownCancellationTokenSource.Dispose();
-		semaphore.Dispose();
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceTicketManager.cs b/Agent/Phantom.Agent.Services/Instances/InstanceTicketManager.cs
new file mode 100644
index 0000000..b195644
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/Instances/InstanceTicketManager.cs
@@ -0,0 +1,100 @@
+using Phantom.Agent.Rpc;
+using Phantom.Common.Data;
+using Phantom.Common.Data.Agent;
+using Phantom.Common.Data.Instance;
+using Phantom.Common.Data.Replies;
+using Phantom.Common.Messages.Agent.ToController;
+using Phantom.Utils.Logging;
+using Phantom.Utils.Tasks;
+using Serilog;
+
+namespace Phantom.Agent.Services.Instances;
+
+sealed class InstanceTicketManager {
+	private static readonly ILogger Logger = PhantomLogger.Create<InstanceTicketManager>();
+	
+	private readonly AgentInfo agentInfo;
+	private readonly ControllerConnection controllerConnection;
+	
+	private readonly HashSet<Guid> activeTicketGuids = new ();
+	private readonly HashSet<ushort> usedPorts = new ();
+	private RamAllocationUnits usedMemory = new ();
+
+	public InstanceTicketManager(AgentInfo agentInfo, ControllerConnection controllerConnection) {
+		this.agentInfo = agentInfo;
+		this.controllerConnection = controllerConnection;
+	}
+
+	public Result<Ticket, LaunchInstanceResult> Reserve(InstanceConfiguration configuration) {
+		var memoryAllocation = configuration.MemoryAllocation;
+		var serverPort = configuration.ServerPort;
+		var rconPort = configuration.RconPort;
+		
+		if (!agentInfo.AllowedServerPorts.Contains(serverPort)) {
+			return LaunchInstanceResult.ServerPortNotAllowed;
+		}
+
+		if (!agentInfo.AllowedRconPorts.Contains(rconPort)) {
+			return LaunchInstanceResult.RconPortNotAllowed;
+		}
+		
+		lock (this) {
+			if (activeTicketGuids.Count + 1 > agentInfo.MaxInstances) {
+				return LaunchInstanceResult.InstanceLimitExceeded;
+			}
+
+			if (usedMemory + memoryAllocation > agentInfo.MaxMemory) {
+				return LaunchInstanceResult.MemoryLimitExceeded;
+			}
+			
+			if (usedPorts.Contains(serverPort)) {
+				return LaunchInstanceResult.ServerPortAlreadyInUse;
+			}
+
+			if (usedPorts.Contains(rconPort)) {
+				return LaunchInstanceResult.RconPortAlreadyInUse;
+			}
+
+			var ticket = new Ticket(Guid.NewGuid(), memoryAllocation, serverPort, rconPort);
+			
+			activeTicketGuids.Add(ticket.TicketGuid);
+			usedMemory += memoryAllocation;
+			usedPorts.Add(serverPort);
+			usedPorts.Add(rconPort);
+			
+			RefreshAgentStatus();
+			Logger.Debug("Reserved ticket {TicketGuid} (server port {ServerPort}, rcon port {RconPort}, memory allocation {MemoryAllocation} MB).", ticket.TicketGuid, ticket.ServerPort, ticket.RconPort, ticket.MemoryAllocation.InMegabytes);
+
+			return ticket;
+		}
+	}
+
+	public bool IsValid(Ticket ticket) {
+		lock (this) {
+			return activeTicketGuids.Contains(ticket.TicketGuid);
+		}
+	}
+	
+	public void Release(Ticket ticket) {
+		lock (this) {
+			if (!activeTicketGuids.Remove(ticket.TicketGuid)) {
+				return;
+			}
+
+			usedMemory -= ticket.MemoryAllocation;
+			usedPorts.Remove(ticket.ServerPort);
+			usedPorts.Remove(ticket.RconPort);
+			
+			RefreshAgentStatus();
+			Logger.Debug("Released ticket {TicketGuid} (server port {ServerPort}, rcon port {RconPort}, memory allocation {MemoryAllocation} MB).", ticket.TicketGuid, ticket.ServerPort, ticket.RconPort, ticket.MemoryAllocation.InMegabytes);
+		}
+	}
+	
+	public void RefreshAgentStatus() {
+		lock (this) {
+			controllerConnection.Send(new ReportAgentStatusMessage(activeTicketGuids.Count, usedMemory));
+		}
+	}
+	
+	public sealed record Ticket(Guid TicketGuid, RamAllocationUnits MemoryAllocation, ushort ServerPort, ushort RconPort);
+}
diff --git a/Agent/Phantom.Agent.Services/Instances/PortManager.cs b/Agent/Phantom.Agent.Services/Instances/PortManager.cs
deleted file mode 100644
index a98c40c..0000000
--- a/Agent/Phantom.Agent.Services/Instances/PortManager.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using Phantom.Common.Data;
-using Phantom.Common.Data.Instance;
-
-namespace Phantom.Agent.Services.Instances;
-
-sealed class PortManager {
-	private readonly AllowedPorts allowedServerPorts;
-	private readonly AllowedPorts allowedRconPorts;
-	private readonly HashSet<ushort> usedPorts = new ();
-
-	public PortManager(AllowedPorts allowedServerPorts, AllowedPorts allowedRconPorts) {
-		this.allowedServerPorts = allowedServerPorts;
-		this.allowedRconPorts = allowedRconPorts;
-	}
-
-	public Result Reserve(InstanceConfiguration configuration) {
-		var serverPort = configuration.ServerPort;
-		var rconPort = configuration.RconPort;
-		
-		if (!allowedServerPorts.Contains(serverPort)) {
-			return Result.ServerPortNotAllowed;
-		}
-
-		if (!allowedRconPorts.Contains(rconPort)) {
-			return Result.RconPortNotAllowed;
-		}
-		
-		lock (usedPorts) {
-			if (usedPorts.Contains(serverPort)) {
-				return Result.ServerPortAlreadyInUse;
-			}
-
-			if (usedPorts.Contains(rconPort)) {
-				return Result.RconPortAlreadyInUse;
-			}
-
-			usedPorts.Add(serverPort);
-			usedPorts.Add(rconPort);
-		}
-
-		return Result.Success;
-	}
-	
-	public void Release(InstanceConfiguration configuration) {
-		lock (usedPorts) {
-			usedPorts.Remove(configuration.ServerPort);
-			usedPorts.Remove(configuration.RconPort);
-		}
-	}
-
-	public enum Result {
-		Success,
-		ServerPortNotAllowed,
-		ServerPortAlreadyInUse,
-		RconPortNotAllowed,
-		RconPortAlreadyInUse
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/Procedures/BackupInstanceProcedure.cs b/Agent/Phantom.Agent.Services/Instances/Procedures/BackupInstanceProcedure.cs
deleted file mode 100644
index abb7e48..0000000
--- a/Agent/Phantom.Agent.Services/Instances/Procedures/BackupInstanceProcedure.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Phantom.Agent.Services.Backups;
-using Phantom.Agent.Services.Instances.States;
-using Phantom.Common.Data.Backups;
-
-namespace Phantom.Agent.Services.Instances.Procedures;
-
-sealed record BackupInstanceProcedure(BackupManager BackupManager) : IInstanceProcedure {
-	private readonly TaskCompletionSource<BackupCreationResult> resultCompletionSource = new ();
-
-	public Task<BackupCreationResult> Result => resultCompletionSource.Task;
-
-	public async Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) {
-		if (context.CurrentState is not InstanceRunningState runningState || runningState.Process.HasEnded) {
-			resultCompletionSource.SetResult(new BackupCreationResult(BackupCreationResultKind.InstanceNotRunning));
-			return null;
-		}
-
-		try {
-			var result = await BackupManager.CreateBackup(context.ShortName, runningState.Process, cancellationToken);
-			resultCompletionSource.SetResult(result);
-		} catch (OperationCanceledException) {
-			resultCompletionSource.SetCanceled(cancellationToken);
-		} catch (Exception e) {
-			resultCompletionSource.SetException(e);
-		}
-
-		return null;
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/Procedures/IInstanceProcedure.cs b/Agent/Phantom.Agent.Services/Instances/Procedures/IInstanceProcedure.cs
deleted file mode 100644
index 0e1e37d..0000000
--- a/Agent/Phantom.Agent.Services/Instances/Procedures/IInstanceProcedure.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Phantom.Agent.Services.Instances.States;
-
-namespace Phantom.Agent.Services.Instances.Procedures;
-
-interface IInstanceProcedure {
-	Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken);
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/Procedures/LaunchInstanceProcedure.cs b/Agent/Phantom.Agent.Services/Instances/Procedures/LaunchInstanceProcedure.cs
deleted file mode 100644
index 1038f09..0000000
--- a/Agent/Phantom.Agent.Services/Instances/Procedures/LaunchInstanceProcedure.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using Phantom.Agent.Minecraft.Instance;
-using Phantom.Agent.Minecraft.Launcher;
-using Phantom.Agent.Minecraft.Server;
-using Phantom.Agent.Services.Instances.States;
-using Phantom.Common.Data.Instance;
-
-namespace Phantom.Agent.Services.Instances.Procedures;
-
-sealed record LaunchInstanceProcedure(Guid InstanceGuid, InstanceConfiguration Configuration, IServerLauncher Launcher, bool IsRestarting = false) : IInstanceProcedure {
-	public async Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) {
-		if (!IsRestarting && context.CurrentState is InstanceRunningState) {
-			return null;
-		}
-		
-		context.SetStatus(IsRestarting ? InstanceStatus.Restarting : InstanceStatus.Launching);
-
-		InstanceLaunchFailReason? failReason = context.Services.PortManager.Reserve(Configuration) switch {
-			PortManager.Result.ServerPortNotAllowed   => InstanceLaunchFailReason.ServerPortNotAllowed,
-			PortManager.Result.ServerPortAlreadyInUse => InstanceLaunchFailReason.ServerPortAlreadyInUse,
-			PortManager.Result.RconPortNotAllowed     => InstanceLaunchFailReason.RconPortNotAllowed,
-			PortManager.Result.RconPortAlreadyInUse   => InstanceLaunchFailReason.RconPortAlreadyInUse,
-			_                                         => null
-		};
-
-		if (failReason is {} reason) {
-			context.SetLaunchFailedStatusAndReportEvent(reason);
-			return new InstanceNotRunningState();
-		}
-		
-		context.Logger.Information("Session starting...");
-		try {
-			InstanceProcess process = await DoLaunch(context, cancellationToken);
-			return new InstanceRunningState(InstanceGuid, Configuration, Launcher, process, context);
-		} catch (OperationCanceledException) {
-			context.SetStatus(InstanceStatus.NotRunning);
-		} catch (LaunchFailureException e) {
-			context.Logger.Error(e.LogMessage);
-			context.SetLaunchFailedStatusAndReportEvent(e.Reason);
-		} catch (Exception e) {
-			context.Logger.Error(e, "Caught exception while launching instance.");
-			context.SetLaunchFailedStatusAndReportEvent(InstanceLaunchFailReason.UnknownError);
-		}
-		
-		context.Services.PortManager.Release(Configuration);
-		return new InstanceNotRunningState();
-	}
-
-	private async Task<InstanceProcess> DoLaunch(IInstanceContext context, CancellationToken cancellationToken) {
-		cancellationToken.ThrowIfCancellationRequested();
-
-		byte lastDownloadProgress = byte.MaxValue;
-
-		void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) {
-			byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100);
-
-			if (lastDownloadProgress != progress) {
-				lastDownloadProgress = progress;
-				context.SetStatus(InstanceStatus.Downloading(progress));
-			}
-		}
-
-		var launchResult = await Launcher.Launch(context.Logger, context.Services.LaunchServices, OnDownloadProgress, cancellationToken);
-		if (launchResult is LaunchResult.InvalidJavaRuntime) {
-			throw new LaunchFailureException(InstanceLaunchFailReason.JavaRuntimeNotFound, "Session failed to launch, invalid Java runtime.");
-		}
-		else if (launchResult is LaunchResult.CouldNotDownloadMinecraftServer) {
-			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotDownloadMinecraftServer, "Session failed to launch, could not download Minecraft server.");
-		}
-		else if (launchResult is LaunchResult.CouldNotPrepareMinecraftServerLauncher) {
-			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotPrepareMinecraftServerLauncher, "Session failed to launch, could not prepare Minecraft server launcher.");
-		}
-		else if (launchResult is LaunchResult.CouldNotConfigureMinecraftServer) {
-			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotConfigureMinecraftServer, "Session failed to launch, could not configure Minecraft server.");
-		}
-		else if (launchResult is LaunchResult.CouldNotStartMinecraftServer) {
-			throw new LaunchFailureException(InstanceLaunchFailReason.CouldNotStartMinecraftServer, "Session failed to launch, could not start Minecraft server.");
-		}
-
-		if (launchResult is not LaunchResult.Success launchSuccess) {
-			throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch.");
-		}
-
-		context.SetStatus(InstanceStatus.Running);
-		context.ReportEvent(InstanceEvent.LaunchSucceeded);
-		return launchSuccess.Process;
-	}
-
-	private sealed class LaunchFailureException : Exception {
-		public InstanceLaunchFailReason Reason { get; }
-		public string LogMessage { get; }
-
-		public LaunchFailureException(InstanceLaunchFailReason reason, string logMessage) {
-			this.Reason = reason;
-			this.LogMessage = logMessage;
-		}
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/Procedures/SetInstanceToNotRunningStateProcedure.cs b/Agent/Phantom.Agent.Services/Instances/Procedures/SetInstanceToNotRunningStateProcedure.cs
deleted file mode 100644
index c597721..0000000
--- a/Agent/Phantom.Agent.Services/Instances/Procedures/SetInstanceToNotRunningStateProcedure.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Phantom.Agent.Services.Instances.States;
-using Phantom.Common.Data.Instance;
-
-namespace Phantom.Agent.Services.Instances.Procedures;
-
-sealed record SetInstanceToNotRunningStateProcedure(IInstanceStatus Status) : IInstanceProcedure {
-	public Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) {
-		if (context.CurrentState is InstanceRunningState { Process.HasEnded: true }) {
-			context.SetStatus(Status);
-			context.ReportEvent(InstanceEvent.Stopped);
-			return Task.FromResult<IInstanceState?>(new InstanceNotRunningState());
-		}
-		else {
-			return Task.FromResult<IInstanceState?>(null);
-		}
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/State/InstanceLaunchProcedure.cs b/Agent/Phantom.Agent.Services/Instances/State/InstanceLaunchProcedure.cs
new file mode 100644
index 0000000..1308dfa
--- /dev/null
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstanceLaunchProcedure.cs
@@ -0,0 +1,86 @@
+using Phantom.Agent.Minecraft.Instance;
+using Phantom.Agent.Minecraft.Launcher;
+using Phantom.Agent.Minecraft.Server;
+using Phantom.Common.Data.Instance;
+using Phantom.Utils.Tasks;
+
+namespace Phantom.Agent.Services.Instances.State;
+
+static class InstanceLaunchProcedure {
+	public static async Task<InstanceRunningState?> Run(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager ticketManager, InstanceTicketManager.Ticket ticket, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
+		context.Logger.Information("Session starting...");
+
+		Result<InstanceProcess, InstanceLaunchFailReason> result;
+
+		if (ticketManager.IsValid(ticket)) {
+			try {
+				result = await LaunchInstance(context, launcher, reportStatus, cancellationToken);
+			} catch (OperationCanceledException) {
+				reportStatus(InstanceStatus.NotRunning);
+				return null;
+			} catch (Exception e) {
+				context.Logger.Error(e, "Caught exception while launching instance.");
+				result = InstanceLaunchFailReason.UnknownError;
+			}
+		}
+		else {
+			context.Logger.Error("Attempted to launch instance with an invalid ticket!");
+			result = InstanceLaunchFailReason.UnknownError;
+		}
+
+		if (result) {
+			reportStatus(InstanceStatus.Running);
+			context.ReportEvent(InstanceEvent.LaunchSucceeded);
+			return new InstanceRunningState(context, configuration, launcher, ticket, result.Value, cancellationToken);
+		}
+		else {
+			reportStatus(InstanceStatus.Failed(result.Error));
+			context.ReportEvent(new InstanceLaunchFailedEvent(result.Error));
+			return null;
+		}
+	}
+
+	private static async Task<Result<InstanceProcess, InstanceLaunchFailReason>> LaunchInstance(InstanceContext context, IServerLauncher launcher, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
+		cancellationToken.ThrowIfCancellationRequested();
+
+		byte lastDownloadProgress = byte.MaxValue;
+
+		void OnDownloadProgress(object? sender, DownloadProgressEventArgs args) {
+			byte progress = (byte) Math.Min(args.DownloadedBytes * 100 / args.TotalBytes, 100);
+
+			if (lastDownloadProgress != progress) {
+				lastDownloadProgress = progress;
+				reportStatus(InstanceStatus.Downloading(progress));
+			}
+		}
+
+		switch (await launcher.Launch(context.Logger, context.Services.LaunchServices, OnDownloadProgress, cancellationToken)) {
+			case LaunchResult.Success launchSuccess:
+				return launchSuccess.Process;
+
+			case LaunchResult.InvalidJavaRuntime:
+				context.Logger.Error("Session failed to launch, invalid Java runtime.");
+				return InstanceLaunchFailReason.JavaRuntimeNotFound;
+
+			case LaunchResult.CouldNotDownloadMinecraftServer:
+				context.Logger.Error("Session failed to launch, could not download Minecraft server.");
+				return InstanceLaunchFailReason.CouldNotDownloadMinecraftServer;
+
+			case LaunchResult.CouldNotPrepareMinecraftServerLauncher:
+				context.Logger.Error("Session failed to launch, could not prepare Minecraft server launcher.");
+				return InstanceLaunchFailReason.CouldNotPrepareMinecraftServerLauncher;
+
+			case LaunchResult.CouldNotConfigureMinecraftServer:
+				context.Logger.Error("Session failed to launch, could not configure Minecraft server.");
+				return InstanceLaunchFailReason.CouldNotConfigureMinecraftServer;
+
+			case LaunchResult.CouldNotStartMinecraftServer:
+				context.Logger.Error("Session failed to launch, could not start Minecraft server.");
+				return InstanceLaunchFailReason.CouldNotStartMinecraftServer;
+
+			default:
+				context.Logger.Error("Session failed to launch.");
+				return InstanceLaunchFailReason.UnknownError;
+		}
+	}
+}
diff --git a/Agent/Phantom.Agent.Services/Instances/InstanceLogSender.cs b/Agent/Phantom.Agent.Services/Instances/State/InstanceLogSender.cs
similarity index 98%
rename from Agent/Phantom.Agent.Services/Instances/InstanceLogSender.cs
rename to Agent/Phantom.Agent.Services/Instances/State/InstanceLogSender.cs
index 3702416..daac981 100644
--- a/Agent/Phantom.Agent.Services/Instances/InstanceLogSender.cs
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstanceLogSender.cs
@@ -5,7 +5,7 @@ using Phantom.Common.Messages.Agent.ToController;
 using Phantom.Utils.Logging;
 using Phantom.Utils.Tasks;
 
-namespace Phantom.Agent.Services.Instances;
+namespace Phantom.Agent.Services.Instances.State;
 
 sealed class InstanceLogSender : CancellableBackgroundTask {
 	private static readonly BoundedChannelOptions BufferOptions = new (capacity: 64) {
diff --git a/Agent/Phantom.Agent.Services/Instances/States/InstanceRunningState.cs b/Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs
similarity index 60%
rename from Agent/Phantom.Agent.Services/Instances/States/InstanceRunningState.cs
rename to Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs
index b0af65a..3d30b3b 100644
--- a/Agent/Phantom.Agent.Services/Instances/States/InstanceRunningState.cs
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstanceRunningState.cs
@@ -1,37 +1,39 @@
 using Phantom.Agent.Minecraft.Instance;
 using Phantom.Agent.Minecraft.Launcher;
 using Phantom.Agent.Services.Backups;
-using Phantom.Agent.Services.Instances.Procedures;
 using Phantom.Common.Data.Backups;
 using Phantom.Common.Data.Instance;
+using Phantom.Common.Data.Replies;
 
-namespace Phantom.Agent.Services.Instances.States;
+namespace Phantom.Agent.Services.Instances.State;
 
-sealed class InstanceRunningState : IInstanceState, IDisposable {
+sealed class InstanceRunningState : IDisposable {
+	public InstanceTicketManager.Ticket Ticket { get; }
 	public InstanceProcess Process { get; }
 
 	internal bool IsStopping { get; set; }
 
-	private readonly Guid instanceGuid;
+	private readonly InstanceContext context;
 	private readonly InstanceConfiguration configuration;
 	private readonly IServerLauncher launcher;
-	private readonly IInstanceContext context;
+	private readonly CancellationToken cancellationToken;
 
 	private readonly InstanceLogSender logSender;
 	private readonly BackupScheduler backupScheduler;
 
 	private bool isDisposed;
 
-	public InstanceRunningState(Guid instanceGuid, InstanceConfiguration configuration, IServerLauncher launcher, InstanceProcess process, IInstanceContext context) {
-		this.instanceGuid = instanceGuid;
+	public InstanceRunningState(InstanceContext context, InstanceConfiguration configuration, IServerLauncher launcher, InstanceTicketManager.Ticket ticket, InstanceProcess process, CancellationToken cancellationToken) {
+		this.context = context;
 		this.configuration = configuration;
 		this.launcher = launcher;
-		this.context = context;
+		this.Ticket = ticket;
 		this.Process = process;
+		this.cancellationToken = cancellationToken;
 
-		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, instanceGuid, context.ShortName);
+		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, context.InstanceGuid, context.ShortName);
 
-		this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, process, context, configuration.ServerPort);
+		this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
 		this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
 	}
 
@@ -41,7 +43,7 @@ sealed class InstanceRunningState : IInstanceState, IDisposable {
 		if (Process.HasEnded) {
 			if (TryDispose()) {
 				context.Logger.Warning("Session ended immediately after it was started.");
-				context.EnqueueProcedure(new SetInstanceToNotRunningStateProcedure(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)), immediate: true);
+				context.Actor.Tell(new InstanceActor.HandleProcessEndedCommand(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)));
 			}
 		}
 		else {
@@ -60,13 +62,17 @@ sealed class InstanceRunningState : IInstanceState, IDisposable {
 			return;
 		}
 
+		if (cancellationToken.IsCancellationRequested) {
+			return;
+		}
+		
 		if (IsStopping) {
-			context.EnqueueProcedure(new SetInstanceToNotRunningStateProcedure(InstanceStatus.NotRunning), immediate: true);
+			context.Actor.Tell(new InstanceActor.HandleProcessEndedCommand(InstanceStatus.NotRunning));
 		}
 		else {
 			context.Logger.Information("Session ended unexpectedly, restarting...");
 			context.ReportEvent(InstanceEvent.Crashed);
-			context.EnqueueProcedure(new LaunchInstanceProcedure(instanceGuid, configuration, launcher, IsRestarting: true));
+			context.Actor.Tell(new InstanceActor.LaunchInstanceCommand(configuration, launcher, Ticket, IsRestarting: true));
 		}
 	}
 
@@ -74,16 +80,16 @@ sealed class InstanceRunningState : IInstanceState, IDisposable {
 		context.ReportEvent(new InstanceBackupCompletedEvent(e.Kind, e.Warnings));
 	}
 
-	public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
+	public async Task<SendCommandToInstanceResult> SendCommand(string command, CancellationToken cancellationToken) {
 		try {
 			context.Logger.Information("Sending command: {Command}", command);
 			await Process.SendCommand(command, cancellationToken);
-			return true;
+			return SendCommandToInstanceResult.Success;
 		} catch (OperationCanceledException) {
-			return false;
+			return SendCommandToInstanceResult.UnknownError;
 		} catch (Exception e) {
 			context.Logger.Warning(e, "Caught exception while sending command.");
-			return false;
+			return SendCommandToInstanceResult.UnknownError;
 		}
 	}
 
@@ -100,7 +106,6 @@ sealed class InstanceRunningState : IInstanceState, IDisposable {
 		backupScheduler.Stop();
 		
 		Process.Dispose();
-		context.Services.PortManager.Release(configuration);
 		
 		return true;
 	}
diff --git a/Agent/Phantom.Agent.Services/Instances/Procedures/StopInstanceProcedure.cs b/Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs
similarity index 67%
rename from Agent/Phantom.Agent.Services/Instances/Procedures/StopInstanceProcedure.cs
rename to Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs
index 09d68eb..6c43968 100644
--- a/Agent/Phantom.Agent.Services/Instances/Procedures/StopInstanceProcedure.cs
+++ b/Agent/Phantom.Agent.Services/Instances/State/InstanceStopProcedure.cs
@@ -1,32 +1,25 @@
 using System.Diagnostics;
 using Phantom.Agent.Minecraft.Command;
 using Phantom.Agent.Minecraft.Instance;
-using Phantom.Agent.Services.Instances.States;
 using Phantom.Common.Data.Instance;
 using Phantom.Common.Data.Minecraft;
 
-namespace Phantom.Agent.Services.Instances.Procedures;
+namespace Phantom.Agent.Services.Instances.State;
 
-sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInstanceProcedure {
+static class InstanceStopProcedure {
 	private static readonly ushort[] Stops = { 60, 30, 10, 5, 4, 3, 2, 1, 0 };
 
-	public async Task<IInstanceState?> Run(IInstanceContext context, CancellationToken cancellationToken) {
-		if (context.CurrentState is not InstanceRunningState runningState) {
-			return null;
-		}
-
+	public static async Task<bool> Run(InstanceContext context, MinecraftStopStrategy stopStrategy, InstanceRunningState runningState, Action<IInstanceStatus> reportStatus, CancellationToken cancellationToken) {
 		var process = runningState.Process;
-
 		runningState.IsStopping = true;
-		context.SetStatus(InstanceStatus.Stopping);
 
-		var seconds = StopStrategy.Seconds;
+		var seconds = stopStrategy.Seconds;
 		if (seconds > 0) {
 			try {
 				await CountDownWithAnnouncements(context, process, seconds, cancellationToken);
 			} catch (OperationCanceledException) {
 				runningState.IsStopping = false;
-				return null;
+				return false;
 			}
 		}
 
@@ -38,14 +31,14 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta
 			}
 		} finally {
 			context.Logger.Information("Session stopped.");
-			context.SetStatus(InstanceStatus.NotRunning);
+			reportStatus(InstanceStatus.NotRunning);
 			context.ReportEvent(InstanceEvent.Stopped);
 		}
 
-		return new InstanceNotRunningState();
+		return true;
 	}
 
-	private async Task CountDownWithAnnouncements(IInstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) {
+	private static async Task CountDownWithAnnouncements(InstanceContext context, InstanceProcess process, ushort seconds, CancellationToken cancellationToken) {
 		context.Logger.Information("Session stopping in {Seconds} seconds.", seconds);
 
 		foreach (var stop in Stops) {
@@ -66,7 +59,7 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta
 		return MinecraftCommand.Say("Server shutting down in " + seconds + (seconds == 1 ? " second." : " seconds."));
 	}
 
-	private async Task DoStop(IInstanceContext context, InstanceProcess process) {
+	private static async Task DoStop(InstanceContext context, InstanceProcess process) {
 		context.Logger.Information("Sending stop command...");
 		await TrySendStopCommand(context, process);
 
@@ -74,7 +67,7 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta
 		await WaitForSessionToEnd(context, process);
 	}
 
-	private async Task TrySendStopCommand(IInstanceContext context, InstanceProcess process) {
+	private static async Task TrySendStopCommand(InstanceContext context, InstanceProcess process) {
 		using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
 		try {
 			await process.SendCommand(MinecraftCommand.Stop, timeout.Token);
@@ -87,10 +80,9 @@ sealed record StopInstanceProcedure(MinecraftStopStrategy StopStrategy) : IInsta
 		}
 	}
 
-	private async Task WaitForSessionToEnd(IInstanceContext context, InstanceProcess process) {
-		using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55));
+	private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
 		try {
-			await process.WaitForExit(timeout.Token);
+			await process.WaitForExit(TimeSpan.FromSeconds(55));
 		} catch (OperationCanceledException) {
 			try {
 				context.Logger.Warning("Waiting timed out, killing session...");
diff --git a/Agent/Phantom.Agent.Services/Instances/States/IInstanceState.cs b/Agent/Phantom.Agent.Services/Instances/States/IInstanceState.cs
deleted file mode 100644
index 6acc25e..0000000
--- a/Agent/Phantom.Agent.Services/Instances/States/IInstanceState.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Phantom.Agent.Services.Instances.States;
-
-interface IInstanceState {
-	void Initialize();
-	Task<bool> SendCommand(string command, CancellationToken cancellationToken);
-}
diff --git a/Agent/Phantom.Agent.Services/Instances/States/InstanceNotRunningState.cs b/Agent/Phantom.Agent.Services/Instances/States/InstanceNotRunningState.cs
deleted file mode 100644
index 3d009fd..0000000
--- a/Agent/Phantom.Agent.Services/Instances/States/InstanceNotRunningState.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Phantom.Agent.Services.Instances.States;
-
-sealed class InstanceNotRunningState : IInstanceState {
-	public void Initialize() {}
-
-	public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
-		return Task.FromResult(false);
-	}
-}
diff --git a/Agent/Phantom.Agent.Services/Rpc/ControllerMessageHandlerActor.cs b/Agent/Phantom.Agent.Services/Rpc/ControllerMessageHandlerActor.cs
index 91e4cc9..c89b893 100644
--- a/Agent/Phantom.Agent.Services/Rpc/ControllerMessageHandlerActor.cs
+++ b/Agent/Phantom.Agent.Services/Rpc/ControllerMessageHandlerActor.cs
@@ -1,4 +1,5 @@
-using Phantom.Common.Data.Instance;
+using Phantom.Agent.Services.Instances;
+using Phantom.Common.Data.Instance;
 using Phantom.Common.Data.Replies;
 using Phantom.Common.Messages.Agent;
 using Phantom.Common.Messages.Agent.BiDirectional;
@@ -57,7 +58,7 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
 		connection.SetIsReady();
 		
 		await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All));
-		await agent.InstanceSessionManager.RefreshAgentStatus();
+		agent.InstanceTicketManager.RefreshAgentStatus();
 	}
 
 	private void HandleRegisterAgentFailure(RegisterAgentFailureMessage message) {
@@ -74,7 +75,7 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
 	}
 	
 	private Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message, bool alwaysReportStatus) {
-		return agent.InstanceSessionManager.Configure(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus);
+		return agent.InstanceManager.Request(new InstanceManagerActor.ConfigureInstanceCommand(message.InstanceGuid, message.Configuration, message.LaunchProperties, message.LaunchNow, alwaysReportStatus));
 	}
 	
 	private async Task<InstanceActionResult<ConfigureInstanceResult>> HandleConfigureInstance(ConfigureInstanceMessage message) {
@@ -82,15 +83,15 @@ public sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToAgent
 	}
 
 	private async Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
-		return await agent.InstanceSessionManager.Launch(message.InstanceGuid);
+		return await agent.InstanceManager.Request(new InstanceManagerActor.LaunchInstanceCommand(message.InstanceGuid));
 	}
 
 	private async Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
-		return await agent.InstanceSessionManager.Stop(message.InstanceGuid, message.StopStrategy);
+		return await agent.InstanceManager.Request(new InstanceManagerActor.StopInstanceCommand(message.InstanceGuid, message.StopStrategy));
 	}
 
 	private async Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
-		return await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command);
+		return await agent.InstanceManager.Request(new InstanceManagerActor.SendCommandToInstanceCommand(message.InstanceGuid, message.Command));
 	}
 
 	private void HandleReply(ReplyMessage message) {
diff --git a/Agent/Phantom.Agent/Program.cs b/Agent/Phantom.Agent/Program.cs
index e4156a9..0543439 100644
--- a/Agent/Phantom.Agent/Program.cs
+++ b/Agent/Phantom.Agent/Program.cs
@@ -58,10 +58,8 @@ try {
 	var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection));
 	await agentServices.Initialize();
 
-	using var actorSystem = ActorSystemFactory.Create("Agent");
-	
 	var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource);
-	var rpcMessageHandlerActor = actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
+	var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
 	
 	var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
 	var rpcTask = RpcClientRuntime.Launch(rpcSocket, rpcMessageHandlerActor, rpcDisconnectSemaphore, shutdownCancellationToken);
diff --git a/Common/Phantom.Common.Data/Instance/IInstanceStatus.cs b/Common/Phantom.Common.Data/Instance/IInstanceStatus.cs
index dafadec..b1d937e 100644
--- a/Common/Phantom.Common.Data/Instance/IInstanceStatus.cs
+++ b/Common/Phantom.Common.Data/Instance/IInstanceStatus.cs
@@ -52,6 +52,18 @@ public static class InstanceStatus {
 	public static IInstanceStatus Invalid(string reason) => new InstanceIsInvalid(reason);
 	public static IInstanceStatus Downloading(byte progress) => new InstanceIsDownloading(progress);
 	public static IInstanceStatus Failed(InstanceLaunchFailReason reason) => new InstanceIsFailed(reason);
+
+	public static bool IsLaunching(this IInstanceStatus status) {
+		return status is InstanceIsDownloading or InstanceIsLaunching or InstanceIsRestarting;
+	}
+
+	public static bool IsRunning(this IInstanceStatus status) {
+		return status is InstanceIsRunning;
+	}
+	
+	public static bool IsStopping(this IInstanceStatus status) {
+		return status is InstanceIsStopping;
+	}
 	
 	public static bool CanLaunch(this IInstanceStatus status) {
 		return status is InstanceIsNotRunning or InstanceIsFailed;
diff --git a/Common/Phantom.Common.Data/Instance/InstanceLaunchFailReason.cs b/Common/Phantom.Common.Data/Instance/InstanceLaunchFailReason.cs
index 149f43c..80cc5ae 100644
--- a/Common/Phantom.Common.Data/Instance/InstanceLaunchFailReason.cs
+++ b/Common/Phantom.Common.Data/Instance/InstanceLaunchFailReason.cs
@@ -2,10 +2,6 @@
 
 public enum InstanceLaunchFailReason : byte {
 	UnknownError                           = 0,
-	ServerPortNotAllowed                   = 1,
-	ServerPortAlreadyInUse                 = 2,
-	RconPortNotAllowed                     = 3,
-	RconPortAlreadyInUse                   = 4,
 	JavaRuntimeNotFound                    = 5,
 	CouldNotDownloadMinecraftServer        = 6,
 	CouldNotConfigureMinecraftServer       = 7,
diff --git a/Common/Phantom.Common.Data/Replies/LaunchInstanceResult.cs b/Common/Phantom.Common.Data/Replies/LaunchInstanceResult.cs
index d5ee83d..d81322b 100644
--- a/Common/Phantom.Common.Data/Replies/LaunchInstanceResult.cs
+++ b/Common/Phantom.Common.Data/Replies/LaunchInstanceResult.cs
@@ -5,5 +5,9 @@ public enum LaunchInstanceResult : byte {
 	InstanceAlreadyLaunching = 2,
 	InstanceAlreadyRunning   = 3,
 	InstanceLimitExceeded    = 4,
-	MemoryLimitExceeded      = 5
+	MemoryLimitExceeded      = 5,
+	ServerPortNotAllowed     = 6,
+	ServerPortAlreadyInUse   = 7,
+	RconPortNotAllowed       = 8,
+	RconPortAlreadyInUse     = 9
 }
diff --git a/Common/Phantom.Common.Data/Replies/SendCommandToInstanceResult.cs b/Common/Phantom.Common.Data/Replies/SendCommandToInstanceResult.cs
index 34c86bd..57292ac 100644
--- a/Common/Phantom.Common.Data/Replies/SendCommandToInstanceResult.cs
+++ b/Common/Phantom.Common.Data/Replies/SendCommandToInstanceResult.cs
@@ -1,6 +1,7 @@
 namespace Phantom.Common.Data.Replies;
 
 public enum SendCommandToInstanceResult : byte {
-	UnknownError,
-	Success
+	UnknownError = 0,
+	Success = 1,
+	InstanceNotRunning = 2
 }
diff --git a/Controller/Phantom.Controller.Database/Repositories/UserRepository.cs b/Controller/Phantom.Controller.Database/Repositories/UserRepository.cs
index 59b94c8..597e99d 100644
--- a/Controller/Phantom.Controller.Database/Repositories/UserRepository.cs
+++ b/Controller/Phantom.Controller.Database/Repositories/UserRepository.cs
@@ -100,7 +100,7 @@ public sealed class UserRepository {
 
 		user.PasswordHash = UserPasswords.Hash(password);
 
-		return Result.Ok<SetUserPasswordError>();
+		return Result.Ok;
 	}
 
 	public void DeleteUser(UserEntity user) {
diff --git a/Controller/Phantom.Controller.Services/Phantom.Controller.Services.csproj b/Controller/Phantom.Controller.Services/Phantom.Controller.Services.csproj
index 7eaaadf..167c6af 100644
--- a/Controller/Phantom.Controller.Services/Phantom.Controller.Services.csproj
+++ b/Controller/Phantom.Controller.Services/Phantom.Controller.Services.csproj
@@ -15,7 +15,6 @@
     <ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
     <ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" />
     <ProjectReference Include="..\..\Utils\Phantom.Utils.Actor\Phantom.Utils.Actor.csproj" />
-    <ProjectReference Include="..\..\Utils\Phantom.Utils.Events\Phantom.Utils.Events.csproj" />
     <ProjectReference Include="..\Phantom.Controller.Database\Phantom.Controller.Database.csproj" />
     <ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" />
   </ItemGroup>
diff --git a/Utils/Phantom.Utils.Actor/ActorRef.cs b/Utils/Phantom.Utils.Actor/ActorRef.cs
index 89444a0..259d9b8 100644
--- a/Utils/Phantom.Utils.Actor/ActorRef.cs
+++ b/Utils/Phantom.Utils.Actor/ActorRef.cs
@@ -29,6 +29,10 @@ public readonly struct ActorRef<TMessage> {
 		return Request(message, timeout: null, cancellationToken);
 	}
 
+	public Task<bool> Stop(TMessage message, TimeSpan? timeout = null) {
+		return actorRef.GracefulStop(timeout ?? Timeout.InfiniteTimeSpan, message);
+	}
+	
 	public Task<bool> Stop(TimeSpan? timeout = null) {
 		return actorRef.GracefulStop(timeout ?? Timeout.InfiniteTimeSpan);
 	}
diff --git a/Utils/Phantom.Utils/Processes/Process.cs b/Utils/Phantom.Utils/Processes/Process.cs
index 5c42370..cdb598d 100644
--- a/Utils/Phantom.Utils/Processes/Process.cs
+++ b/Utils/Phantom.Utils/Processes/Process.cs
@@ -37,8 +37,8 @@ public sealed class Process : IDisposable {
 
 		// https://github.com/dotnet/runtime/issues/81896
 		if (OperatingSystem.IsWindows()) {
-			Task.Factory.StartNew(ReadStandardOutputSynchronously, TaskCreationOptions.LongRunning);
-			Task.Factory.StartNew(ReadStandardErrorSynchronously, TaskCreationOptions.LongRunning);
+			Task.Factory.StartNew(ReadStandardOutputSynchronously, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+			Task.Factory.StartNew(ReadStandardErrorSynchronously, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
 		}
 		else {
 			this.wrapped.BeginOutputReadLine();
@@ -79,7 +79,11 @@ public sealed class Process : IDisposable {
 	}
 
 	public Task WaitForExitAsync(CancellationToken cancellationToken) {
-		return wrapped.WaitForExitAsync(cancellationToken);
+		try {
+			return wrapped.WaitForExitAsync(cancellationToken);
+		} catch (InvalidOperationException) {
+			return Task.CompletedTask;
+		}
 	}
 
 	public void Kill(bool entireProcessTree = false) {
diff --git a/Utils/Phantom.Utils/Tasks/Result.cs b/Utils/Phantom.Utils/Tasks/Result.cs
index eba7082..4988ef4 100644
--- a/Utils/Phantom.Utils/Tasks/Result.cs
+++ b/Utils/Phantom.Utils/Tasks/Result.cs
@@ -41,14 +41,16 @@ public abstract record Result<TError> {
 	public static implicit operator Result<TError>(TError error) {
 		return new Fail(error);
 	}
+
+	public static implicit operator Result<TError>(Result.OkType _) {
+		return new Ok();
+	}
 	
 	public static implicit operator bool(Result<TError> result) {
 		return result is Ok;
 	}
 	
 	public sealed record Ok : Result<TError> {
-		internal static Ok Instance { get; } = new ();
-		
 		public override TError Error {
 			get => throw new InvalidOperationException("Attempted to get error from Ok result.");
 			init {}
@@ -59,19 +61,7 @@ public abstract record Result<TError> {
 }
 
 public static class Result {
-	public static Result<TError> Ok<TError>() {
-		return Result<TError>.Ok.Instance;
-	}
-	
-	public static Result<TError> Fail<TError>(TError error) {
-		return new Result<TError>.Fail(error);
-	}
-	
-	public static Result<TValue, TError> Ok<TValue, TError>(TValue value) {
-		return new Result<TValue, TError>.Ok(value);
-	}
-	
-	public static Result<TValue, TError> Fail<TValue, TError>(TError error) {
-		return new Result<TValue, TError>.Fail(error);
-	}
+	public static OkType Ok { get; }  = new ();
+
+	public readonly record struct OkType;
 }
diff --git a/Web/Phantom.Web/Pages/Setup.razor b/Web/Phantom.Web/Pages/Setup.razor
index a4a730c..62a52ed 100644
--- a/Web/Phantom.Web/Pages/Setup.razor
+++ b/Web/Phantom.Web/Pages/Setup.razor
@@ -90,7 +90,7 @@
   private async Task<Result<string>> CreateOrUpdateAdministrator() {
     var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUserMessage(form.Username, form.Password), Timeout.InfiniteTimeSpan);
     return reply switch {
-           Success             => Result.Ok<string>(),
+           Success             => Result.Ok,
            CreationFailed fail => fail.Error.ToSentences("\n"),
            UpdatingFailed fail => fail.Error.ToSentences("\n"),
            AddingToRoleFailed  => "Could not assign administrator role to user.",
diff --git a/Web/Phantom.Web/Utils/Messages.cs b/Web/Phantom.Web/Utils/Messages.cs
index 46a23d4..ab2681d 100644
--- a/Web/Phantom.Web/Utils/Messages.cs
+++ b/Web/Phantom.Web/Utils/Messages.cs
@@ -57,16 +57,16 @@ static class Messages {
 			LaunchInstanceResult.InstanceAlreadyRunning   => "Instance is already running.",
 			LaunchInstanceResult.InstanceLimitExceeded    => "Agent does not have any more available instances.",
 			LaunchInstanceResult.MemoryLimitExceeded      => "Agent does not have enough available memory.",
+			LaunchInstanceResult.ServerPortNotAllowed     => "Server port not allowed.",
+			LaunchInstanceResult.ServerPortAlreadyInUse   => "Server port already in use.",
+			LaunchInstanceResult.RconPortNotAllowed       => "Rcon port not allowed.",
+			LaunchInstanceResult.RconPortAlreadyInUse     => "Rcon port already in use.",
 			_                                             => "Unknown error."
 		};
 	}
 
 	public static string ToSentence(this InstanceLaunchFailReason reason) {
 		return reason switch {
-			InstanceLaunchFailReason.ServerPortNotAllowed                   => "Server port not allowed.",
-			InstanceLaunchFailReason.ServerPortAlreadyInUse                 => "Server port already in use.",
-			InstanceLaunchFailReason.RconPortNotAllowed                     => "Rcon port not allowed.",
-			InstanceLaunchFailReason.RconPortAlreadyInUse                   => "Rcon port already in use.",
 			InstanceLaunchFailReason.JavaRuntimeNotFound                    => "Java runtime not found.",
 			InstanceLaunchFailReason.CouldNotDownloadMinecraftServer        => "Could not download Minecraft server.",
 			InstanceLaunchFailReason.CouldNotConfigureMinecraftServer       => "Could not configure Minecraft server.",
@@ -78,8 +78,9 @@ static class Messages {
 
 	public static string ToSentence(this SendCommandToInstanceResult reason) {
 		return reason switch {
-			SendCommandToInstanceResult.Success => "Command sent.",
-			_                                   => "Unknown error."
+			SendCommandToInstanceResult.Success            => "Command sent.",
+			SendCommandToInstanceResult.InstanceNotRunning => "Instance is not running.",
+			_                                              => "Unknown error."
 		};
 	}