Guide·2026-10-19·16 min

DataStore Lock Lost (Error 267) on Roblox: What It Means and How to Fix It

DataStore lock lost (Error 267) on Roblox is not a ban. What it means, why the server lost its lock, and how to fix it in Lyra, ProfileStore or your code.

S

Sametcan Tasgiran

Founder & Developer · Picoo

The message "DataStore lock lost, please rejoin the game." means the game server lost its claim on your saved data, so it stopped saving your progress and kicked you. It is not a ban: Error Code 267 means the game's own script sent the kick, so you can rejoin right away, though progress since the last save may be gone. The exact text comes from Lyra, an open-source player-data library by Paradoxum Games, the studio behind Tower Defense Simulator; if it is your game, the steps below show why your server stopped renewing the lock.

What "DataStore lock lost" means

Your save lives in a DataStore, and a session lock decides which server may write it. The server that loads your data claims the lock and has to keep renewing it while you play. If it cannot, another server may take the lock, and the old server must stop writing, because its copy may be out of date. Roblox's player data guide says:

If a different server takes the lock, attempts by the current server to read or write the key fail unless it establishes a new lock.

Roblox does not lock player data for you, and its data store error list has no lock errors, so every "lock lost" kick comes from the game's own code or from a library it uses. Lyra kicks with a fixed message, and the Roblox client shows it like this:

You have been kicked by this experience or its moderators. Moderation message: DataStore lock lost, please rejoin the game. (Error Code: 267)

Roblox uses this same box for every kick that a game script sends with its own message, so "Moderation message" does not mean a moderator was involved.

Any game built on Lyra's PlayerStore kicks with these exact words. Paradoxum Games moved Tower Defense Simulator's player data to Lyra in its v1.38.4 patch (late October 2024), part of the Hexscape Night II update. The game's v1.80.0 update log (12 November 2025) says its Lyra system was enhanced "with data-store session locking for better stability". A TDS player posted a screenshot of this exact kick on the TDS wiki in June 2025.

If you got kicked as a player

  • Rejoin. You are not banned, and your account is fine.
  • Some recent progress may be gone. Lyra drops anything that changed since the game's last save, and it autosaves every 5 minutes.
  • If rejoining shows "DataStore load failed, please rejoin the game.", the new server could not get your data yet. Wait a minute or two, then try again.
  • Play on one device at a time. Joining from a second device kicks the first one with a different error (273) and makes the new server wait for your save.
  • If lots of players get kicked at the same moment, check status.roblox.com for a Roblox incident and wait until it is fixed before you rejoin.
  • If it keeps happening, tell the game's developer. Only they can see the logs that explain it.

Why a server loses the lock

In Lyra

Lyra keeps each player's lock in a MemoryStore HashMap for 90 seconds and renews it every 60 seconds. The lock counts as lost when:

  • A renewal fails with a MemoryStore error.
  • A renewal finds another server's lock id on the key. With Lyra's default lock, that only happens after the 90 seconds ran out and a second server took it.
  • Lyra's own 90 second timer runs out before a renewal succeeds, for example because MemoryStore calls hang or the server stalls.

Lyra makes up to 5 attempts at each renewal, waiting 2, 4, 8 and 16 seconds between them, but it only tries again when the error text contains TotalRequestsOverLimit, PartitionRequestsOverLimit, RequestThrottled, Throttled, Timeout or InternalError. Any other error ends the lock on the first failure. The waits add up to 30 seconds, the whole gap between the 60 second renewal and the 90 second lock, so a renewal that needs all five attempts loses the lock anyway.

When the lock is lost, Lyra closes the session without a final save and kicks the player, so everything since the last save is lost.

MemoryStore trouble hits this lock directly. Every renewal is a HashMap UpdateAsync, which uses at least 2 request units from your game's MemoryStore budget: 1000 plus 120 for every player in your experience (all servers together), per minute. Your own queues, sorted maps and other MemoryStore calls use the same budget, and when the game goes over it, renewals get throttled. During a MemoryStore incident that Roblox confirmed on 6 September 2025, a developer whose game used MemoryStore session locks reported every player being kicked, then kicked again after rejoining.

In any save system

  • Another server took over after the lock expired. A lock expires when its owner does not renew it in time: a renewal interval at or above the expiry, an outage, throttled request queues, a PlayerRemoving handler that yields before the final save, or a degraded server. Roblox notes that a player's data is most likely to end up on two servers when they hop servers quickly, for example while teleporting.
  • Something wiped the lock. Admin tools, migrations or other scripts that write the key with a plain SetAsync replace its metadata, and an UpdateAsync that does not return the old metadata clears it.
  • Two scripts opened the same player's key on one server. In a homemade lock, or with two copies of a library module, each one claims the key and one of them loses. ProfileStore throws an error instead, and Lyra reuses the open session or rejects the second load.
  • The session ended for a normal reason. ProfileStore's OnSessionEnd also fires after your own EndSession and at shutdown, so code that kicks on every OnSessionEnd shows a "rejoin" message at the wrong time.

Find the code that kicks

Press Ctrl+Shift+F in Studio (Cmd+Shift+F on Mac) to open Find All, and search for the exact words of the kick you saw. If they are in Lyra's PlayerStore, go to the Lyra section below. Other libraries use different words or none, so if nothing matches, search for ProfileStore, DataStoreModule, Lapis and :Kick( to find the library or script that ends the session. This is how each library signals a lost lock:

  • Lyra: PlayerStore kicks with "DataStore lock lost, please rejoin the game." and, at join, "DataStore load failed, please rejoin the game."
  • ProfileStore: Profile.OnSessionEnd fires. The official tutorial kicks with "Profile session end - Please rejoin".
  • Suphi's DataStore Module: the session's State turns false.
  • Lapis: an error that says "The session lock was stolen".

Fix it in Lyra

Lyra has no setting for this kick or its message, and changing them means forking Lyra (it is MIT licensed). The steps below make lock losses easier to trace and make each one cost players less progress.

Log why the lock was lost

Without a logCallback, Lyra throws its log lines away. This server script prints Lyra's warnings and errors as plain text, gives Studio its own store name and closes the store at shutdown, a step Lyra leaves to you. It works with Lyra 0.6.0, the current Wally release.

-- Server Script in ServerScriptService
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Lyra = require(ReplicatedStorage.Packages.Lyra) -- change this to where Lyra is in your game

local store = Lyra.createPlayerStore({
	-- Studio gets its own store, so play tests never load or save live data
	name = if RunService:IsStudio() then "PlayerData_Dev" else "PlayerData",
	template = { coins = 0 },
	schema = function(data)
		return typeof(data.coins) == "number", "coins must be a number"
	end,
	-- Without this, Lyra throws its logs away and you never see why a lock was lost
	logCallback = function(message)
		if message.level == "warn" or message.level == "error" or message.level == "fatal" then
			-- Turn the context table into text, because live servers print a table as "table: 0x..."
			local parts = {}
			for key, value in message.context or {} do
				table.insert(parts, `{key}={tostring(value)}`)
			end
			warn(`[Lyra][{message.level}] {message.message} {table.concat(parts, " ")}`)
		end
	end,
})

local function onPlayerAdded(player)
	store:loadAsync(player) -- kicks with "DataStore load failed" if it cannot load
end

Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do -- players who joined before this script ran
	task.spawn(onPlayerAdded, player)
end

Players.PlayerRemoving:Connect(function(player)
	store:unloadAsync(player) -- final save, then the lock is released
end)

game:BindToClose(function()
	store:closeAsync() -- Lyra does not do this for you
end)

Then read the warnings. On a live server, press F9 (or type /console in chat) to open the Developer Console and switch its log to Server; only the game's owner and group members with edit permission can see that view. To check every server at once, open Creator Dashboard > Creations > your game > Monitoring > Error Report, which lists server errors and warnings.

  • failed to refresh lock, lock considered lost with an error=HashMap error: ... part on the same line means the MemoryStore call failed.
  • The same line with no error= part means the renewal found another server's lock id on the key.
  • lock expiry timer fired or refresh failed, invoking onLockLost callbacks with no failed to refresh lock line just before it means the 90 second timer ran out before a renewal finished.
  • The lock was lost, closing session and stopping autosave and lock lost for key, removing session lines that follow are normal for every lock loss.

If MemoryStore errors show up for many players at the same moment, check status.roblox.com for a Roblox incident before you change your code.

Keep Studio off your live data

The default MemoryStore lock does not protect live data from Studio. Studio's MemoryStore is kept apart from live servers, so a play test never sees the live lock, but with API access on, Studio loads and saves the same DataStore records as the live game. A Studio session and a live server can then hold the same player at once and overwrite each other's saves, and nobody gets kicked. That is why the script above gives Studio its own store name. Roblox also advises turning on Studio API access only in a separate test copy of your experience.

Save right after important changes

A lost lock drops everything since the last autosave, so save on the spot after purchases, trades and match results:

store:updateAsync(player, function(data)
	data.coins += 100
	return true -- commit the change
end)
store:saveAsync(player) -- write it now instead of at the next autosave

Give the lock room to renew

Open Creator Dashboard > Creations > your game > Monitoring > Memory Stores. If the API Request Unit chart is close to your quota, or the Request Count by Status chart shows errors at the times players were kicked, cut your own MemoryStore calls, for example sorted-map reads in a loop. Lyra's renewals need at least 2 request units per player per minute, out of the 120 that each player adds to your budget.

Keep Lyra's default lock unless MemoryStore failures keep causing the kicks. Lyra's GitHub main branch has an opt-in useDSSLocking = true that keeps the lock in a DataStore instead and costs DataStore requests, and Wally 0.6.0 silently ignores it. If you use it, give Studio its own store name as in the script above, because main-branch Lyra ignores existing locks in Studio, so a Studio play test with API access could take your live lock and get you kicked from the live game.

Fix it in ProfileStore or Suphi's module

ProfileStore reports a lost lock through Profile.OnSessionEnd, which also fires after your own EndSession and at shutdown. OnLastSave tells them apart: it passes "Manual", "External" or "Shutdown", and it never fires when another server steals the session, as ProfileStore's API docs explain.

Replace the official tutorial's profile.OnSessionEnd handler, the one that kicks with "Profile session end - Please rejoin", with this one:

-- Inside your PlayerAdded code, right after StartSessionAsync returned a profile
local lastSaveReason = nil
profile.OnLastSave:Connect(function(reason) -- "Manual", "External" or "Shutdown"
	lastSaveReason = reason
end)

profile.OnSessionEnd:Connect(function()
	Profiles[player] = nil -- stop all reads and writes for this player
	if lastSaveReason == "Manual" or lastSaveReason == "Shutdown" then
		return -- your EndSession or a server shutdown: no kick needed
	end
	-- "External": another server asked for the profile; nil: it was stolen
	warn(`[Data] session lost for {player.UserId}: {lastSaveReason or "stolen"}`)
	if player.Parent == Players then
		player:Kick("Your data was opened on another server. Please rejoin.")
	end
end)
  • In Studio, use your store's .Mock (for example PlayerStore = PlayerStore.Mock) so play tests never take a live session.
  • Never load player data with { Steal = true }. ProfileStore's docs say not to use it for normal loads.
  • Change profile.Data only while profile:IsActive() is true.

Suphi's DataStore Module closes the session after 5 failed MemoryStore renewals (LockAttempts, with a 60 second LockInterval), and State turns false. Create the object only inside PlayerAdded, as its docs say, and write only while State == true. The author's player example reopens the session in a loop instead of kicking:

-- From the module author's player data example.
-- DataStoreModule and template are the ones your script already has.
local Players = game:GetService("Players")

local function StateChanged(state, dataStore)
	while dataStore.State == false do -- keep trying to reopen while the session is closed
		if dataStore:Open(template) ~= "Success" then
			task.wait(6)
		end
	end
end

Players.PlayerAdded:Connect(function(player)
	local dataStore = DataStoreModule.new("Player", player.UserId)
	dataStore.StateChanged:Connect(StateChanged)
	StateChanged(dataStore.State, dataStore)
end)

Players.PlayerRemoving:Connect(function(player)
	local dataStore = DataStoreModule.find("Player", player.UserId)
	if dataStore ~= nil then
		dataStore:Destroy() -- saves, closes the session and ends the loop above
	end
end)

Fix it in your own session lock

Roblox's design keeps the lock in the key's metadata and writes it inside the same UpdateAsync call that reads the key. The script below does that and follows these rules:

  • One new GUID per join, reused on every retry, because a DataStore call that errored may still have written.
  • An ownership check in every save. If the lock id is not yours, the lock is lost: write nothing and stop saving that player.
  • An expiry several save intervals long. Every save renews the lock, and Roblox's best practices say the save interval must be shorter than the lock expiry.
  • A random start time for each player's autosave and random jitter on retries, so servers that start together do not all write at the same moment.
  • A release when the player leaves, even in the middle of loading, and in BindToClose, which gets at most 30 seconds.
  • No lock checks with GetAsync, which reads from a 4 second cache and is not atomic with the write.

Before you paste it into a live game, change "PlayerData", the "User_" key prefix and { Coins = 0 } to the store name, key format and default data your game already uses, or every player loads a fresh save.

-- Server Script in ServerScriptService
local DataStoreService = game:GetService("DataStoreService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

-- A separate store in Studio keeps play tests away from live saves and locks
local store = DataStoreService:GetDataStore(if RunService:IsStudio() then "PlayerData_Dev" else "PlayerData")
local SAVE_INTERVAL = 60 -- every save also renews the lock
local LOCK_EXPIRY = 300 -- a lock nobody renewed for this long counts as dead
local sessions = {} -- [player] = { key, userId, lockId, data, closed }

-- mode is "acquire", "save", "release" or "unlock"; returns "ok", "locked", "lost" or "error"
local function lockedUpdate(key, userId, lockId, mode, data)
	local outcome = "error"
	local ok, result = pcall(store.UpdateAsync, store, key, function(current, keyInfo)
		local meta = if keyInfo then keyInfo:GetMetadata() else {}
		local userIds = if keyInfo then keyInfo:GetUserIds() else {}
		if #userIds == 0 then
			userIds = { userId } -- Roblox recommends tagging each key with its player (GDPR)
		end
		local age = if keyInfo then os.time() - keyInfo.UpdatedTime // 1000 else math.huge
		if mode == "acquire" then
			if meta.LockId ~= nil and meta.LockId ~= lockId and age < LOCK_EXPIRY then
				outcome = "locked"
				return nil -- another server holds a live lock: write nothing
			end
			meta.LockId = lockId
			outcome = "ok"
			return current or data, userIds, meta
		end
		if meta.LockId ~= lockId then
			outcome = "lost" -- taken over, or the lock was wiped
			return nil -- never overwrite data this server no longer owns
		end
		outcome = "ok"
		if mode == "release" then
			meta.LockId = nil
		elseif mode == "unlock" then
			meta.LockId = nil
			return current, userIds, meta -- drop our lock, keep the stored data as it is
		end
		return data, userIds, meta
	end)
	if not ok then
		return "error", result -- the write may still have happened: retry with the same lockId
	end
	return outcome, result
end

local function autosave(player, s)
	if lockedUpdate(s.key, s.userId, s.lockId, "save", s.data) == "lost" and not s.closed then
		s.closed = true
		sessions[player] = nil -- lock lost: stop every write for this player
		warn(`[Data] lock lost for {s.userId}`)
		if player.Parent == Players then
			player:Kick("Your data was opened on another server. Please rejoin.")
		end
	end
end

local function startAutosave(player, s)
	-- A random first wait, so servers that start together do not all save at the same moment
	task.delay(math.random() * SAVE_INTERVAL, function()
		while sessions[player] == s and not s.closed do
			autosave(player, s)
			task.wait(SAVE_INTERVAL)
		end
	end)
end

local function onPlayerAdded(player)
	local userId = player.UserId
	local key = "User_" .. userId
	local lockId = HttpService:GenerateGUID(false) -- one id per join, reused on retries
	local giveUpAt = os.clock() + LOCK_EXPIRY + 60
	local attempt = 0
	while os.clock() < giveUpAt do
		attempt += 1
		local outcome, data = lockedUpdate(key, userId, lockId, "acquire", { Coins = 0 })
		if player.Parent ~= Players then -- left while loading
			if outcome ~= "locked" then -- "ok", or an "error" whose write may still have landed
				for try = 1, 3 do
					if lockedUpdate(key, userId, lockId, "unlock") ~= "error" then
						break
					end
					task.wait(2 ^ try + math.random())
				end
			end
			return
		end
		if outcome == "ok" then
			local s = { key = key, userId = userId, lockId = lockId, data = data, closed = false }
			sessions[player] = s
			startAutosave(player, s)
			return
		end
		task.wait(math.min(2 ^ attempt, 30) + math.random()) -- "locked" or "error": wait, then try again
	end
	player:Kick("Your data is still being saved by another server. Please rejoin in a minute.")
end

local function release(player)
	local s = sessions[player]
	if s == nil or s.closed then
		return
	end
	s.closed = true
	for attempt = 1, 5 do -- this is the last save, so retry it
		if lockedUpdate(s.key, s.userId, s.lockId, "release", s.data) ~= "error" then
			break
		end
		task.wait(2 ^ attempt + math.random())
	end
	sessions[player] = nil
end

Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerRemoving:Connect(release)

game:BindToClose(function() -- Roblox gives this at most 30 seconds
	for player in sessions do
		task.spawn(release, player)
	end
	local deadline = os.clock() + 25
	while next(sessions) ~= nil and os.clock() < deadline do
		task.wait(0.5)
	end
end)

Change a player's data only through sessions[player].data while the session exists, for example:

local s = sessions[player]
if s then
	s.data.Coins += 10 -- the next autosave or the leave save writes it
end

If other scripts need the data, turn this into a ModuleScript that a server Script requires, and give it a get(player) function.

To try it in Studio, publish a separate test copy of your experience and turn on Enable Studio Access to API Services there (File > Experience Settings > Security). Roblox advises against turning it on for a live game, because then every script you run in Studio reads and writes live data. In a place that was never published, GetDataStore errors and the script stops.

The trade-off is that when a server crashes, its players cannot load anywhere else until LOCK_EXPIRY seconds have passed since its last save. ProfileStore shortens that wait by messaging the holding server and stealing after 40 seconds.

Mistakes that make it worse

  • Writing a player's key outside the lock code. Any other UpdateAsync on that key must return keyInfo:GetUserIds() and keyInfo:GetMetadata(), and admin edits should happen while the player is offline.
  • Teleporting while the old server still holds the lock. Legacy ProfileService releases the profile and teleports inside ListenToHopReady. With other libraries, make the old server save and release as soon as the player leaves so the new place waits less, and load progress from your DataStore on arrival, never from teleport data, as the teleport guide explains.
  • Turning the lock off, or loading with ProfileStore's Steal, to stop the kicks. Without a lock, two servers can write the same player's data, which is how items get duplicated and newer progress gets overwritten. The developer who disabled their lock during the September 2025 incident reported that several players could have suffered data corruption or data loss.

How Picoo handles session locks

Picoo is a Roblox AI game maker: you chat in your browser, and its Roblox Studio plugin builds the result into your place. Ask it to add a session lock to your player data, and its data-saving instructions tell it to build one. In the module those instructions describe, every write goes through UpdateAsync, and a save that finds another server's lock writes nothing and kicks the player. On a game you already have, the same instructions tell it to repair your save code in place and keep your DataStore name and key format.

Its basic save recipe uses a plain SetAsync with no lock, as the DataStore saving guide says, and the tycoon kit ships with ProfileStore. You still test leaving and rejoining on a live server yourself. See how it works at picoo.io.

FAQ

What does DataStore lock lost mean on Roblox?

The game server lost its session lock on your save, so it stopped saving and kicked you. The exact message comes from Lyra, the player-data library that Tower Defense Simulator uses.

Is a data lock lost kick on Roblox a ban?

No. Error code 267 means the game's own script kicked you, and you can rejoin. Progress since the game's last save may be lost.

How do I fix DataStore lock lost in my Roblox game?

Find which library kicks, log why the lock was lost, close the store in BindToClose, save right after important changes and keep MemoryStore use within budget. In a lock you wrote yourself, check ownership in every save and stop writing once the lock is not yours.

What is error code 267 on Roblox?

It is Roblox's code for a kick sent by a game's own script (DisconnectLuaKick). The text after "Moderation message" comes from that game's code and says why it removed you.

Want to try Picoo?

Premium tool for serious Roblox developers. Pro from $12/mo billed yearly — cancel anytime. Plugin installs in two minutes.

Get Pro →