Great tutorial, please post more content like this in the future, it’s pretty awesome.
And this is why I would rather store my data on my custom database implementation.
Thank you for writing this wonderful guide! I’m quite saddened by both the fact that this behavior was not previously documented by Roblox engineers and by the fact that it has yet to be corrected.
Most developers fully rely on DataStoreService to store all of their game-related data. I cannot fathom the difficulties faced by the beginner developer which is attempting to fulfill a seemingly simple task of storing player data. It’s as such no surprise that data-loss errors are so common amongst the developer community.
Edited the figure: I was informed Roblox uses Amazon DynamoDB instead of what I had written there first.
Excellent research. I hope we migrate this information into the developer hub.
Sweet, this really helped me out to understand the DataStoreService
much better. I have done Datastores before but they were really basic, this is a really good tutorial.
okey dokey
The actual size limit for values appears to be 2^18-1
, but only after the value has been coverted to JSON.
-- JSON-encoded string is enclosed in double-quotes, so 2 characters must be subtracted.
print(pcall(function() game:GetService("DataStoreService"):GetDataStore("test"):SetAsync("test", string.rep("A",2^18-3)) end))
--> true
print(pcall(function() game:GetService("DataStoreService"):GetDataStore("test"):SetAsync("test", string.rep("A",2^18-2)) end))
--> false 105: Serialized value converted byte size exceeds max size 64*1024 bytes.
The error is also off by several powers of 2.
The key limit is apparently 49 rather than 50:
print(pcall(function() game:GetService("DataStoreService"):GetDataStore("test"):GetAsync(string.rep("A",50)) end))
--> false 102: Key name exceeds the 50 character limit.
print(pcall(function() game:GetService("DataStoreService"):GetDataStore("test"):GetAsync(string.rep("A",49)) end))
--> true
There is certainly a lonely <
searching its missing =
friend.
Getting an accurate size can be a lot more involved that it seems. All non-regular characters appear to be escaped as \uXXXX
, which can inflate the length of a character 6 times in some cases. Verifying string lengths isn’t helped by JSONEncode only accepting tables, either.
Datastore functions don’t accept strings with characters above 127 (should be a bug).
print(pcall(function() return game:GetService("DataStoreService"):GetDataStore("test"):SetAsync("test","\128") end))
--> false 104: Cannot store string in DataStore
Perhaps the string is (incorrectly) assumed to be unicode encoding rather than unencoded bytes?
It should also be mentioned that, unlike most of the Lua API, datastore functions properly handle null characters within strings.
print(pcall(function() local ds = game:GetService("DataStoreService"):GetDataStore("test") ds:SetAsync("test","hello\0world") return #ds:GetAsync("test") end))
--> true 11
Oops, I threw in that size limit section last minute while writing the draft just to have it there, I forgot I had that whole section labeled as “complete and accurate”. The subsections after that one are complete
EDIT: I edited/changed all of this information, thanks!
Is this going to be fixed anytime soon?
I thought I should ask because you might know or able to acquire news about this broken feature everyone is relying on and want fixed.
Very Informative Thread!
This isn’t a bug, strings in data stores explicitly only store valid UTF-8.
I developed a global marketplace about a month ago for my game, but couldn’t ever ship it because it only worked if both players were in the same server (aka not global. Defeats the purpose) This OnUpdate bug seems to be the issue. No wonder. I hope this gets fixed soon. If it does, someone please @ me.
You can subscribe to notifications on the linked bug report and you’ll get an email/browser notification when someone replies there.
This is quite possibly the best guide I’ve ever read on anything related to ROBLOX.
Awesome job, I wish I had this years ago, it would’ve saved me a lot of time.
Heads up, there’s a bug going around right now related to potential data loss if you try to call the same key in succession within a short period of time (<4s between attempts), more info here.
Are you sure that set requests do not set the read cache? I believe they do.
local services = {
data_store = game:GetService("DataStoreService")
}
--local data_store = services.data_store:GetDataStore("name_0")
--local data_store = services.data_store:GetDataStore("name_1")
local data_store = services.data_store:GetDataStore("name_2")
game.Players.PlayerAdded:Connect(function(player)
wait(10)
local value = data_store:GetAsync("key")
data_store:SetAsync("key", value == nil and 1 or value + 1)
end)
while true do
wait(0.5)
local value = data_store:GetAsync("key")
game.ReplicatedStorage.RemoteEvent:FireAllClients("Value at: " .. os.time() .. " is " .. tostring(value))
end
game.ReplicatedStorage.RemoteEvent.OnClientEvent:Connect(function(message)
print(message)
end)
Assuming wait()
calls are not taking much longer than the cache time for the thread to be resumed (which they shouldn’t be and aren’t in my testing), this code should always print that the value is the initial value, and never get the incremented value since I am resetting the cache cooldown every ~0.5 seconds. However, in my testing, it does receive the new value.
Perhaps I’m missing something obvious, but it sure seems like GlobalDataStore:SetAsync()
does indeed set the read cache for the key.
That aside, great thread! Very helpful. Shame that DataStore
s are so needlessly complex due to the odd caching mechanism.
All key writes that you do on a particular server will be immediately available on that server. Perhaps I could describe this more clearly, but with “setting the cache” I mean that it sets the cache time (the value is irrelevant in that description). For example, doing an UpdateAsync and then a GetAsync afterwards makes the GetAsync not consume any budget, since UpdateAsync set the cache time. SetAsync does not set the get cache time, so the GetAsync call after that will consume budget, even though the server should know what the most recent value is because of the recent SetAsync.
Ah, the things we learn when we try to reverse engineer and emulate a black-box system. I know what that’s like. Really great work.
Updated topic with new max size limit.
The limit is actually 2^22-1
, or 4,194,303. You forgot to include the encoded double-quotes from the string you used to check the size limit.
I noticed an inaccuracy. In this, you state that the documentation is wrong about the 50 character limit for keys. I don’t know if this was true in the past, but it doesn’t seem to be anymore.
In this image, you can see the error happens on line 14 and not 13.