Data store

Data stores are a storage feature for Roblox games that can be used to save data that will persist even after a game’s server has shut down. They are designed to store custom player data or to control global game configurations. Data stores are shared per game, so any place in a game, including places in different servers, can access and change the same data.

Structure[edit]

A data store is essentially a dictionary, like a Lua table. Each value in the data store can be indexed by a unique key. For example, if a game needed to save how much experience each player had, it could add to a data store using the player’s user id as the key and the player’s experience as the value. Any place that needed to know how much experience a given player has could access the data store and look up that player’s experience with that player’s id.

DataStoreStructure.png

A data store is accessed by its name with the GetDataStore function.

local DataStoreService = game:GetService("DataStoreService")
local playerExperienceStore = DataStoreService:GetDataStore("PlayerExperience")
Data stores can only be accessed by the server through Scripts. Trying to access a data store through a client in a LocalScript will cause an error.

Along with a name, a scope can also be defined for a data store. The scope is the name of a collection of data stores. This is mainly used for organization as it allows for multiple data stores with the same name. For example, suppose players in a game had both statistics and resources, and the game needed to store this data in two data stores per player. Each player’s user id could be used as a scope and “Resources” and “Stats” could be used as the data store names.

DataStoreScopeStructure.png
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
 
local function onPlayerAdded(player)
	local scope = "player_" .. player.UserId
	local playerStats = DataStoreService:GetDataStore("Stats", scope)
	local playerResources = DataStoreService:GetDataStore("Resources", scope)
end
 
Players.PlayerAdded:Connect(onPlayerAdded)

Note that scope is an optional parameter to GetDataStore. If no scope is provided, GetDataStore will use a scope called “global” automatically.

Managing Data[edit]

Saving Data[edit]

Data stores have a function called SetAsync which can be used to set the value of an entry in a data store. This function takes the key of the entry and the value to set as parameters.

local DataStoreService = game:GetService("DataStoreService")
local playerExperienceStore = DataStoreService:GetDataStore("PlayerExperience")
 
pcall(function()
	playerExperienceStore:SetAsync("player_1234", 50)
end)
Note that SetAsync can be hazardous to use as it completely overwrites whatever was in the entry at the time. UpdateAsync is recommend for most use cases as it takes into account the old value before making any changes.
Note that any functions that access a data store’s contents, such as SetAsync, GetAsync, and UpdateAsync, are all web calls and have the potential to fail. It is strongly recommended that any such call be wrapped with a pcall to catch and handle any errors.

Reading Data[edit]

Data stores have a function called GetAsync which can be used to read the value of an entry in a data store. This function takes the key of the entry to retrieve as its only parameter.

local DataStoreService = game:GetService("DataStoreService")
local playerExperienceStore = DataStoreService:GetDataStore("PlayerExperience")
 
local playerExperience = 0
pcall(function()
	playerExperience = playerExperienceStore:GetAsync("player_1234")
end)
To save bandwidth, GetAsync will cache its last lookup for about 4 seconds.

Updating Data[edit]

There are two methods of updating data store data that respect the data that was already stored: IncrementAsync and UpdateAsync.

IncrementAsync[edit]

IncrementAsync is used to change the numerical value stored in a data store. This function takes two arguments: a string that is the key to the entry, and a number indicating how much to change the value.

local DataStoreService = game:GetService("DataStoreService")
local visitsStore = DataStoreService:GetDataStore("PlayerVisits")
 
local success, visits = pcall(function()
	return visitsStore:IncrementAsync("player_1234", 1)
end)
if success then
	print("visits:", visits)
end

If the call to IncrementAsync is successful then it will return the new value of the stored number. If IncrementAsync is called on a key with no value, then a new value of 0 will be created for that key (which will then be incremented by the function).

UpdateAsync[edit]

UpdateAsync is used to change a stored value in a data store. Unlike IncrementAsync, UpdateAsync can be used to change any type of value (such as strings or tables). UpdateAsync takes the key of the entry to update, as well as a function as its second parameter. This function defines how the entry should be updated. It takes the old value of the entry as a parameter and must return the new value of the entry.

local DataStoreService = game:GetService("DataStoreService")
local nicknameStore = DataStoreService:GetDataStore("NicknameStore")
 
local function makeNameLower(oldName)
	local newName = string.lower(oldName)
	return newName
end
 
local success, newName = pcall(function()
	return nicknameStore:UpdateAsync("player_1234", makeNameLower)
end)
 
if success then
	print("Lower cased name:", newName)
end
The function passed into UpdateAsync is not permitted to yield. This means that cannot use any yield function such as wait or WaitForChild. If the function yields, then UpdateAsync will throw an error and the value will not be updated.

Events[edit]

Each key in a data store has an event that can be listened to that fires any time the value changes; this can be connected to with the OnUpdate function. This function takes two arguments: the name of the key and a function to call when the entry associated with the key changes.

local DataStoreService = game:GetService("DataStoreService")
local playerExperienceStore = DataStoreService:GetDataStore("PlayerExperience")
 
local connection
 
local function onPlayerXPUpdate(newXP)
	print("player_1234's xp is now:", newXP)
	if newXP > 50 then
		connection:Disconnect()
	end
end
 
connection = playerExperienceStore:OnUpdate("player_1234", onPlayerXPUpdate)

OnUpdate returns an RBXScriptConnection, the same type of instance that binding to events returns. This is mainly used so that the event can stop being listed to with the Disconnect function.

It is highly recommended to disconnect OnUpdate events when they are no longer needed so the OnUpdate request limit does not get reached.

Error Handling[edit]

Data stores are built on top of Roblox’s backend servers to store data. This means that anytime a data store is read from or written to, several web calls have to be made. These calls may occasionally fail due to poor connectivity or any other number of issues. Therefore it is important to make sure that any call made to read or update a data store has proper error handling with pcalls. If this is not in place, then any data store function that fails will cause an error which will stop execution of the thread the function was called in.

List of Errors[edit]

Every error message is accompanied with an error code, which you could parse to get the type of error. A list of all errors can be found here.

Example[edit]

Suppose a game had a currency system and stored each player’s money in a data store. A simple implementation might look something like this:

-- Data store calls without proper error handling
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
 
local playerMoneyStore = DataStoreService:GetDataStore("PlayerMoneyStore")
 
local playerMoney = {}
 
local function onPlayerAdded(player)
	local playerId = tostring(player.UserId)
	playerMoney[player] = playerMoneyStore:GetAsync(playerId)
	-- Do other player initialization code
end
 
Players.PlayerAdded:Connect(onPlayerAdded)

The above does two things wrong. First, the code does not catch any errors that GetAsync might throw. If onPlayerAdded had any other code after the GetAsync call, it would not run if there was an error.

Second, and related to error handling, the player is given no indication if there was a problem retrieving their data. If a data store call fails the player should be notified, especially if the game uses data stores for other features (as those might be experiencing problems as well).

The above code can be improved simply by adding a pcall and a RemoteEvent (to notify a client if there are problems).

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") 
local playerMoneyStore = DataStoreService:GetDataStore("PlayerMoneyStore")
 
local playerNotificationEvent = Instance.new("RemoteEvent")playerNotificationEvent.Name = "PlayerNotificationEvent"playerNotificationEvent.Parent = ReplicatedStorage 
local playerMoney = {}
 
local function onPlayerAdded(player)
	local playerId = tostring(player.UserId)
	local success, money = pcall(function()		return playerMoneyStore:GetAsync(playerId)	end)	if success then		playerMoney[player] = money	else		playerNotificationEvent:FireClient(player, "Error retrieving saved data: please try again later")	end 
	-- Do other player initialization code
end
 
Players.PlayerAdded:Connect(onPlayerAdded)

Limitations[edit]

The Roblox data backend has limits on how often it can be used to ensure good performance across all game servers. These limits are reflected in the Roblox API. If a game uses data stores more than it is allowed, the engine will automatically throttle data store usage for that game, which will cause data store requests to take longer. With that in mind, it is important to know the limitations of data stores to avoid unnecessary performance issues.

Per Place Limit[edit]

Each place in a game is budgeted a certain number of data store requests based on the number of players that are present (more players means more data will be needed).

The limit rates for a place are based on the kind of request being made.

Request type Methods Requests allowed per minute
Gets GetAsync 60 + numPlayers * 10
Sets SetAsync, IncrementAsync, UpdateAsync 60 + numPlayers * 10
GetSorted (per page) GetSortedAsync 5 + numPlayers * 2
OnUpdate OnUpdate 30 + numPlayers * 5
Request type Methods Cooldown
Write Requests (same key) Repeated SetAsync, IncrementAsync, UpdateAsync on same key 6 second between writes

For example, suppose a game regularly calls SetAsync in a server with two players. This place would be able to make 80 set requests per minute (60 + 2 players * 10 requests per player).

If a place surpasses any of these rates, then the methods of that request type will be throttled until the number of requests goes down beneath the limit. Note that only the type of request that was surpassed will be throttled; if other requests are within their limit they will execute normally.

Game Limits[edit]

Along with place specific limits, there are also limits on how often an entire game (all of a game’s active servers) can call data stores. Specifically, requests to a specific key can be throttled if it is being requested from too many places at once. A key in this case is defined by the combination of the data store’s scope, data store’s name, and the index being requested. If a game requests the same key too many times, then its requests can be throttled or even error.

Consider the following example. In this game, a data store is used to record the total number of visits for the game. The code might look something like this:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
 
local globalStats = DataStoreService:GetDataStore("GlobalStats")
globalStats:IncrementAsync("Visits", 1)
 
local function onPlayerAdded(player)
	local success, visits = pcall(function()
		return globalStats:IncrementAsync("Visits", 1)
	end)
	if success then
		print("The game has been visited:", visits, "times.")
	end
end
 
Players.PlayerAdded:Connect(onPlayerAdded)

On a place by place basis, the above code would not be a problem. IncrementAsync is only being called once per player, and each place is allotted a fair number of such requests. The issue here though is that the name is not unique. Every server in the game that calls this code will be hitting the same exact key: the index of “Visits” in the “GlobalStats” data store in the “global” scope. If a game has hundreds or thousands of servers, the number of requests going to this key would get quite large and will very likely be throttled or error.

To avoid the above situation, it is recommended only to use data stores in two contexts: saving player data and managing global configuration values. For player data, the keys being requested can be unique per player (the player’s userId is a good value for this) and as such there is little chance of hitting any global limit. For global configurations, if each server is simply reading a value that isn’t expected to change often, there is little chance of hitting the threshold.

Data Limits[edit]

Along with the frequency of requests, data stores also limit how much data can be used per entry. The key, name, and scope of data stores all must be within a certain length, and the amount of data that can be stored is also limited.

Component Maximum number of characters
Key 50
Name 50
Scope 50
Data 260000

Since keys, names, and scopes are all strings, their length can be checked with the string.len function. Data is also saved as a string in data stores, regardless of its initial type. The size of data can be gotten by using HttpService’s JSONEncode function, which will convert Lua data into a serialized JSON table.

For example, suppose a RPG was saving a player’s data as a table with several fields. The size of the stored table could be checked with the above JSONEncode function:

local HttpService = game:GetService("HttpService")
 
local playerData = {}
playerData.Class = "Warrior"
playerData.Gold = 20
playerData.Stats = {}
playerData.Stats.Strength = 16
playerData.Stats.Dexterity = 12
playerData.Stats.Intelligence = 8
 
local dataString = HttpService:JSONEncode(playerData)
print(dataString)
print(string.len(dataString))
 
-- prints:
-- {"Gold":20,"Stats":{"Intelligence":8,"Strength":16,"Dexterity":12},"Class":"Warrior"}
-- 85

Using Data Stores in Studio[edit]

By default, places simulated in Studio do not have access to data stores. Any data store request function (e.g. GetAsync, SetAsync, etc) will cause an error if they are called from Studio. Data stores can be enabled in Studio by turning on the game’s “Enable Studio Access to API Services” setting. This setting can be accessed through roblox.com/develop, clicking Configure Game on the desired game and toggling the setting in the Basic Settings tab.

ConfigureGame.png


EnableStudioAPIAccess.png
Accessing data stores in Studio can be dangerous for live games. Studio accesses the same data stores that the client application does, so there is a possibility of overwriting production data. It is therefore recommended not to enable this setting for live games - rather it should be enabled in separate test games.

See Also: