Skip to content

SoftShutdown

SoftShutdown is the one-line answer to update deploys: when Roblox shuts a server down, players teleport through a reserved transit server and back into a fresh public server instead of being dumped to the app. Combined with the DataDriver’s shutdown flush: data saves, players come back, the update deploys. The defining design decision: the whole flow carries zero persistent state — the only coordination between the two stages is a ChloeKernelTransit flag riding the teleport data.

-- src/Server/Bootstrap.luau
local Root = game:GetService("ServerScriptService").ChloeKernelServer
local SoftShutdown = require(Root.SoftShutdown)
return function(kernel)
SoftShutdown.attach(kernel)
end

That is the entire integration. attach deliberately no-ops in Studio — there is nothing to migrate, and Studio cannot teleport anyway.

attach wires two stages into the same place. Which stage a server plays depends on what kind of server it is — every server gets Stage 1; reserved servers additionally run Stage 2.

A BindToClose callback runs when Roblox shuts the server down:

  1. If nobody is on the server, return immediately — nothing to migrate.
  2. Publish Kernel.SoftShutdown with the player count on the bus. This fires before any teleport — it is your window to message players.
  3. TeleportService:ReserveServer(game.PlaceId) reserves a private server of the same place. If the reservation fails, warn and give up — players disconnect normally, exactly as they would without SoftShutdown.
  4. Build TeleportOptions with the reserved server’s access code and teleport data ChloeKernelTransit = true, then TeleportAsync every player into the transit server in one call. A failed teleport retries once after 1 second; a second failure gives up with a warning.
  5. Wait for the players to actually leave, polling every 0.5s up to a 25-second deadline — Roblox hard-kills the server 30 seconds after BindToClose fires, and the 5-second margin is headroom for everything else sharing that window.

The reserved server is the same place with the same code, so the same attach call runs there. It detects that it is a transit candidate (game.PrivateServerId non-empty with PrivateServerOwnerId == 0 — a reserved server, not a player-owned private server) and bounces arrivals:

  1. For every player present and every player who joins, read Player:GetJoinData().TeleportData.
  2. Only players carrying the ChloeKernelTransit flag bounce. Ordinary reserved servers — Matchmaking arenas, private servers — are left alone; this branch runs on them too, and the flag is what keeps it inert there.
  3. After a 2-second grace delay, TeleportAsync(game.PlaceId, { player }) with no reserved-server option — Roblox’s matchmaker places the player on a fresh public server, which by now is running the new version.
  4. Up to 3 attempts per player with growing backoff (2s, 4s); a player who leaves on their own stops the retries. Three failures warn and leave the player on the transit server.

Stage 2 does not early-return past Stage 1: a deploy can shut down a reserved server too, so transit servers also register the BindToClose migration. A player caught mid-transit during a second deploy just rides another hop.

SoftShutdown and the DataDriver both register BindToClose callbacks, and Roblox gives all bound callbacks one shared 30-second window. The interplay is deliberate:

  • The transit teleport removes players, which fires the normal leave path — session teardown, profile release, save. Most profiles persist through the ordinary save-on-leave flow triggered by the teleport itself.
  • The DataDriver’s own BindToClose flush (releaseAll) catches whatever the leave path has not finished, waiting on its own 25-second deadline and logging any key whose release did not land.
  • Both deadlines sit at 25s inside the 30s hard kill; neither callback depends on the other having run first.

When the player lands on a fresh public server seconds later, the join is an ordinary rejoin: new session, fresh profile load. The closing server released the profile’s session lock during the flush; if the hop is faster than the release, the DataDriver’s session-lock handling applies as it would for any rapid rejoin — see DataDriver.

The Kernel.SoftShutdown topic publishes synchronously before the reservation, so a subscriber can push a state message that reaches clients before the teleport fires:

-- Server: alongside SoftShutdown.attach(kernel)
local Net = kernel:net()
Net:defineState("ServerMessage", { "String" })
kernel.Bus:subscribe("Kernel.SoftShutdown", function(_, playerCount)
Net:broadcastState("ServerMessage", "Updating — teleporting you to the new version…")
end)

Roblox additionally shows its own teleport screen during both hops. There is no built-in transit-server UI; if you want a branded “back in a moment” screen, detect the transit server client-side — the same two signals Stage 2 uses, read with the client-side teleport-data API:

-- src/Client/Bootstrap.luau
local TeleportService = game:GetService("TeleportService")
return function(kernel)
if game.PrivateServerId ~= "" and game.PrivateServerOwnerId == 0 then
local Data = TeleportService:GetLocalPlayerTeleportData()
if type(Data) == "table" and Data.ChloeKernelTransit then
showTransitScreen("Update deployed — sending you back in a moment…")
end
end
end

The stay is short — the bounce fires 2 seconds after arrival — so the screen only needs to cover a few seconds plus the outbound teleport.

The first real update deploy is this feature’s acceptance test. Run it deliberately:

  1. Confirm both participants are wired in the server Bootstrap: SoftShutdown.attach(kernel) and a DataDriver :attach(kernel) (the shutdown flush comes from the driver, not from SoftShutdown).
  2. Publish the update.
  3. Shut down one server from the Creator dashboard’s server list — not Shut Down All Servers — and watch its players resurface on new servers.
  4. Watch server logs for the failure warnings, each of which means a specific fallback fired: [SoftShutdown] ReserveServer failed, [SoftShutdown] transit teleport failed twice, [SoftShutdown] could not bounce <player> back to a public server, and the DataDriver’s unlanded-key log.
  5. Spot-check one migrated player’s data on the new server.
  6. Migrate the rest. Roblox’s Migrate To Latest Update also runs BindToClose on each server, so the same flow covers it.
Member Description
SoftShutdown.attach(kernel) Wires both stages. No-op in Studio. No options, no handle — the module is a BindToClose participant, not a service you interact with afterward
Topic Args Published when
Kernel.SoftShutdown playerCount Update migration is about to teleport everyone out — before the reservation, before any teleport
  • Every failure degrades to the status quo. Failed reservation, double-failed teleport, players stuck past the deadline — each path warns and falls back to what would have happened anyway: a normal disconnect. SoftShutdown can only improve on the default outcome, never worsen it.
  • The transit flag is client-transported, and that is fine. Teleport data round-trips through the client, so a determined exploiter could forge ChloeKernelTransit. The only thing the flag does is bounce its carrier from a reserved server to a public server — a free ride to matchmaking, which is where they could have gone anyway. Nothing trusts the flag for anything of value.
  • Player-owned private servers never bounce. The Stage 2 branch requires PrivateServerOwnerId == 0; VIP servers keep their players.
  • The transit hop doubles as a version fence. Players cannot land back on an old-version public server: by the time the bounce fires, the closing servers are draining and the matchmaker fills servers running the new build.