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
PlayerRemovinghandler 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
SetAsyncreplace its metadata, and anUpdateAsyncthat 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
OnSessionEndalso fires after your ownEndSessionand at shutdown, so code that kicks on everyOnSessionEndshows 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:
PlayerStorekicks with "DataStore lock lost, please rejoin the game." and, at join, "DataStore load failed, please rejoin the game." - ProfileStore:
Profile.OnSessionEndfires. The official tutorial kicks with "Profile session end - Please rejoin". - Suphi's DataStore Module: the session's
Stateturnsfalse. - 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 lostwith anerror=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 callbackswith nofailed to refresh lockline just before it means the 90 second timer ran out before a renewal finished.- The
lock was lost, closing session and stopping autosaveandlock lost for key, removing sessionlines 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 autosaveGive 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 examplePlayerStore = 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.Dataonly whileprofile: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
endIf 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
UpdateAsyncon that key must returnkeyInfo:GetUserIds()andkeyInfo: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.