Managing older servers

As with most games, I intend on updating my game every so often, but I wish to ensure that player’s only join servers containing the updated content as opposed to only the old content. I was considering using a method of booting everyone into a newer version of the server, but as there is a killstreak system in my game, I would not want to ruin people’s active streaks and would rather let the old server “decay”, preventing new players from joining.

That being said, how could I guarantee that this happens? I was recommended a system that would detect if the server the player is joining is outdated using DataStoreService, and then bouncing the player to the newer server, but:

  • I am not sure how to effectively manually update the VersionStore in DataStoreService
  • I don’t want to risk player’s being kicked around a bunch of older servers

The code would look something like this (as recommended to me):

local OutdatedServer = {}

local DataStoreService = game:GetService("DataStoreService")
local VersionStore = DataStoreService:GetDataStore("GameVersion")

local CURRENT_VERSION = 12

function OutdatedServer.isOutdated()
	local ok, latestVersion = pcall(function()
		return VersionStore:GetAsync("Latest")
	end)

	latestVersion = latestVersion or CURRENT_VERSION

	return CURRENT_VERSION < latestVersion
end

function OutdatedServer.getLatest()
	local ok, latestVersion = pcall(function()
		return VersionStore:GetAsync("Latest")
	end)

	return latestVersion or CURRENT_VERSION
end

return OutdatedServer

That being said, what would be the best way of implementing such a function? Would it be better to simply scrap this and replace it with a 10 minute “pre-redirect window”, triggering whenever an update is published, such that players to wrap up their activities before being redirected to updated servers (like how it was done in Pokemon Brick Bronze)?

2 Likes

The best implementation is not “check DataStore on every join and instantly bounce.” That can work, but it is rough and can create loops.

Use a hybrid soft-update system:

  1. Store the latest intended version in DataStoreService.
  2. Broadcast an update signal with MessagingService.
  3. Give players a countdown, for example 10 minutes.
  4. Mark the server as “closing” so new joiners do not start important gameplay.
  5. Teleport players once, with retry handling.
  6. Use Roblox’s built-in Migrate to Latest Update for hard guarantees when needed.

Roblox’s own update docs say migrating players to an updated version works by restarting outdated servers and stopping matchmaking to servers designated for shutdown. (Creator Hub) MessagingService is useful for notifying all live servers about the update because PublishAsync sends a message to subscribers on a topic. (Creator Hub) DataStoreService is fine for storing the authoritative version, and GetAsync is the normal read path, though reads can be briefly cached. (Creator Hub)

My recommendation

Use the 10-minute pre-redirect window as your main flow.

Do not rely only on this:

return CURRENT_VERSION < latestVersion

That tells you the server is old, but it does not solve:

  • where the player should go,
  • whether Roblox will place them into another old public server,
  • whether an old server keeps bouncing them,
  • whether the teleport failed,
  • whether they are in combat/trading/data-save state,
  • whether the server has already begun shutdown.

Instead, make each server enter a controlled state:

local ServerState = {
	IsOutdated = false,
	ShutdownAt = nil,
	Redirecting = {},
}

Then once an update is published, old servers show UI like:

“A new update is available. This server will restart in 10:00.”

At the end, save player state and teleport them.

How to manually update VersionStore

You have three good options.

Option A: Admin-only in-game command

This is the simplest if you trust only yourself/admins.

local DataStoreService = game:GetService("DataStoreService")
local MessagingService = game:GetService("MessagingService")
local Players = game:GetService("Players")

local VersionStore = DataStoreService:GetDataStore("GameVersion")

local ADMINS = {
	[123456789] = true, -- your UserId
}

local UPDATE_TOPIC = "GameUpdate"

local function publishUpdate(newVersion, delaySeconds)
	VersionStore:SetAsync("Latest", {
		version = newVersion,
		publishedAt = os.time(),
		delaySeconds = delaySeconds or 600,
	})

	MessagingService:PublishAsync(UPDATE_TOPIC, {
		version = newVersion,
		publishedAt = os.time(),
		delaySeconds = delaySeconds or 600,
	})
end

Players.PlayerAdded:Connect(function(player)
	player.Chatted:Connect(function(message)
		if not ADMINS[player.UserId] then
			return
		end

		local version = tonumber(message:match("^!update%s+(%d+)$"))
		if version then
			publishUpdate(version, 600)
		end
	end)
end)

Then in-game you type:

!update 13

That updates the DataStore and broadcasts to all live servers.

Option B: Open Cloud / external deployment script

This is better long-term. Roblox Open Cloud lets you access Roblox resources through REST APIs, including data stores, so you can make your deployment pipeline update the version automatically after publishing. (Creator Hub)

Example flow:

Publish Roblox update
↓
Deployment script sets DataStore "GameVersion/Latest" to 13
↓
Deployment script publishes update message
↓
Old servers enter 10-minute shutdown window

This is the cleanest professional setup.

Option C: Roblox Data Stores Manager

Roblox also has a Data Stores Manager for browsing and monitoring data stores from Creator Hub. (Creator Hub) I would use this for debugging, not as your main production update workflow.

Better version object

Do not store just a number. Store a table:

{
	version = 13,
	publishedAt = 1710000000,
	delaySeconds = 600,
	force = false
}

This gives you room to support:

  • normal soft update,
  • forced emergency update,
  • countdown duration,
  • rollback,
  • announcement text,
  • phased updates.

Server-side version module

local DataStoreService = game:GetService("DataStoreService")

local VersionStore = DataStoreService:GetDataStore("GameVersion")

local VersionService = {}

VersionService.CURRENT_VERSION = 12
VersionService.Latest = {
	version = VersionService.CURRENT_VERSION,
	publishedAt = 0,
	delaySeconds = 600,
	force = false,
}

function VersionService:GetLatest()
	local ok, result = pcall(function()
		return VersionStore:GetAsync("Latest")
	end)

	if not ok or result == nil then
		warn("Failed to fetch latest game version:", result)
		return self.Latest
	end

	if typeof(result) == "number" then
		result = {
			version = result,
			publishedAt = 0,
			delaySeconds = 600,
			force = false,
		}
	end

	self.Latest = result
	return result
end

function VersionService:IsOutdated()
	local latest = self:GetLatest()
	return self.CURRENT_VERSION < latest.version
end

return VersionService

Soft shutdown listener

Put this in ServerScriptService.

local MessagingService = game:GetService("MessagingService")
local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")

local VersionService = require(script.Parent.VersionService)

local PLACE_ID = game.PlaceId
local UPDATE_TOPIC = "GameUpdate"

local shuttingDown = false
local shutdownAt = nil
local redirecting = {}

local function notifyPlayers(secondsRemaining)
	-- Replace this with your RemoteEvent UI call.
	print("Server restarting in", secondsRemaining, "seconds")
end

local function teleportPlayer(player)
	if redirecting[player] then
		return
	end

	redirecting[player] = true

	local ok, err = pcall(function()
		TeleportService:TeleportAsync(PLACE_ID, { player })
	end)

	if not ok then
		warn("Teleport failed for", player.Name, err)
		redirecting[player] = nil
		task.delay(10, function()
			if player.Parent and shuttingDown then
				teleportPlayer(player)
			end
		end)
	end
end

local function beginSoftShutdown(updateData)
	if shuttingDown then
		return
	end

	if VersionService.CURRENT_VERSION >= updateData.version then
		return
	end

	shuttingDown = true

	local delaySeconds = updateData.delaySeconds or 600
	shutdownAt = os.time() + delaySeconds

	task.spawn(function()
		while shuttingDown do
			local remaining = math.max(0, shutdownAt - os.time())
			notifyPlayers(remaining)

			if remaining <= 0 then
				break
			end

			task.wait(30)
		end

		for _, player in ipairs(Players:GetPlayers()) do
			teleportPlayer(player)
		end
	end)
end

local ok, err = pcall(function()
	MessagingService:SubscribeAsync(UPDATE_TOPIC, function(message)
		beginSoftShutdown(message.Data)
	end)
end)

if not ok then
	warn("Failed to subscribe to update topic:", err)
end

task.spawn(function()
	task.wait(10)

	if VersionService:IsOutdated() then
		beginSoftShutdown(VersionService.Latest)
	end
end)

Players.PlayerAdded:Connect(function(player)
	if shuttingDown then
		-- Tell them immediately through UI:
		-- "This server is updating. You will be moved shortly."
		task.delay(3, function()
			if player.Parent then
				teleportPlayer(player)
			end
		end)
	end
end)

Preventing players from being kicked around older servers

This is the important part: do not teleport repeatedly just because isOutdated() is true.

Add a TeleportData flag:

local teleportOptions = Instance.new("TeleportOptions")
teleportOptions:SetTeleportData({
	fromVersion = VersionService.CURRENT_VERSION,
	updateRedirect = true,
	redirectedAt = os.time(),
})

Then use:

TeleportService:TeleportAsync(PLACE_ID, { player }, teleportOptions)

On join, read the teleport data:

local joinData = player:GetJoinData()
local teleportData = joinData.TeleportData

if teleportData and teleportData.updateRedirect then
	print(player.Name, "arrived from update redirect")
end

Then apply a cooldown:

local recentlyRedirected = teleportData
	and teleportData.updateRedirect
	and os.time() - teleportData.redirectedAt < 120

if recentlyRedirected and VersionService:IsOutdated() then
	-- Do not instantly bounce again.
	-- Show message and wait, or let Roblox migration handle it.
end

This prevents the worst loop:

Old server → old server → old server → old server

Can you guarantee they always land in a new server?

Not perfectly with normal public TeleportAsync(game.PlaceId, players).

Roblox matchmaking may still place them into an existing server unless Roblox has marked old servers for shutdown/migration. The stronger options are:

Strongest: use Roblox “Migrate to Latest Update”

For important updates, use Roblox’s built-in migration. It is designed to restart outdated servers and stop matchmaking to servers marked for shutdown. (Creator Hub)

Better custom approach: reserved server

You can reserve a server and teleport players there. ReserveServerAsync returns an access code that can be used to teleport players to a reserved server. (Creator Hub)

But for normal live game updates, I would only use reserved servers if you have a lobby/session architecture. For a normal public experience, Roblox’s migration feature is usually the cleaner solution.

Final architecture I’d use

For regular updates:

Publish update
↓
Set VersionStore.Latest = new version
↓
Publish MessagingService update
↓
Old servers show 10-minute countdown
↓
Old servers block new match starts/trades/raids
↓
At countdown end, save data
↓
Teleport players once
↓
If necessary, use Migrate to Latest Update from Creator Dashboard

For emergency breaking updates:

Publish update
↓
Set VersionStore.Latest with force = true
↓
Broadcast immediately
↓
Save player data
↓
Teleport/kick/migrate immediately

My answer to your final question

Yes, I would use the 10-minute pre-redirect window.

I would not scrap the DataStore version system, though. I would use DataStore as the source of truth, MessagingService as the live notification system, and the 10-minute window as the player-friendly migration behavior.

The simple isOutdated() function is fine as a helper, but it should not directly cause an immediate redirect on every join. That is what creates loops and bad UX.

1 Like

Above response is obviously hallucinating with chatgpt

You can check for the difference by comparing the data from MarketplaceService:GetProductInfo(game.PlaceId) to get the most recent version and game.PlaceVersion i think gives you the version of this server

But maybe a better solution is to move everyone to a new server, and you can send killstreak and other necessary data through the teleport to give everyone their stuff back in the new server

5 Likes

I read up a bit on the MarketplaceService documentation and I think what I’ll do is:

  1. Server launched → server stores date and time of creation to itself.
  2. Player joins → server compares date and time of creation to MarketPlaceService:GetProdutInfo(PlaceId).Updated

If the server detects that date and time of creation is inferior to the value produced by .Updated, then it automatically redirects the user to a new server. This way, older servers should “decay” instead of force kicking everyone to newer versions, interrupting gameplay and kill streaks.

That being said, what are your thoughts on this implementation? I fear there may be a risk of kicking the player around a bunch of the same old servers without making a chance of creating a new server, especially during times of low player counts. Is this something I should worry about, and if so, something I can work around?

1 Like

Hey.

The only usage of chatgpt was to format the headers and codeblocks. Approach B is actually what I use now.

Cheers

1 Like

It depends on how complex you want to make the system. If you want to a full migration the options I gave are useful. If you just want to redirect when a player joins the server, the other approach works as well.

Personally, I have a lot of other stuff outside of roblox so the cloud API makes more sense to me. But if we are constraining to within roblox, messaging service with memorystore or data store service is your best bet here.

1 Like

I’m also not really a big fan of just moving the player around since I think it’s poor UX for disrupting the game flow, and players might get discouraged seeing that message (some just quit during that transition). Moreover, I have a game that has an incentive for having friends in the same server, so you’d also have to handle teleport reservations so friends don’t get displaced, which just seems too complex to me given that there’s already a simpler solution.

I’d agree your best bet would be the preventative route; the server decay method + making sure you’re not doing any data overwrites on join that can carry to a newer server.

2 Likes