DataStoreService lets you store data that needs to persist between sessions, such as items in a player's inventory or skill points. Data stores are consistent per experience, so any place in an experience can access and change the same data, including places on different servers.
If you want to add granular permission control to your data stores and access them outside of Studio or Roblox servers, you can use Open Cloud APIs for data stores.
Enabling Studio Access
By default, experiences tested in Studio cannot access data stores, so you must first enable them. Accessing data stores in Studio can be dangerous for live experiences because Studio accesses the same data stores as the client application. To avoid overwriting production data, you should not enable this setting for live experiences — instead, enable it for a separate test version of the experience.
To enable Studio access in a published experience:
- In the Home tab of the menu bar, navigate to the Settings section and click Game Settings. The Game Settings menu displays.
- In the left-hand navigation of the Game Settings menu, click Security.
- Enable the Enable Studio Access to API Services toggle.
- Click the Save button.
Accessing a Data Store
Once you include DataStoreService in a script, access a data store by name using the GetDataStore() function. For example:
local DataStoreService = game:GetService("DataStoreService")local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
Scopes
Every key in a data store has a default "global" scope, but you can further organize keys by setting a unique string as a scope for the second parameter of GetDataStore(). This automatically prepends the scope to all keys in all operations done on the data store.
The scope categorizes your data with a string and a separator with "/", such as:
Key | Scope |
---|---|
houses/User_1234 | houses |
pets/User_1234 | pets |
inventory/User_1234 | inventory |
The combination of datastore name + scope + key uniquely identifies a key and all three values are required to identify a key if it has a scope. For example, a global key named User_1234 can be read as follows:
local DataStoreService = game:GetService("DataStoreService")
local inventoryStore = DataStoreService:GetDataStore("PlayerInventory")
local success, currentGold = pcall(function()
return inventoryStore:GetAsync("User_1234")
end)
By contrast, if key User_1234 has a scope of gold, you can only read it as:
local DataStoreService = game:GetService("DataStoreService")
local inventoryStore = DataStoreService:GetDataStore("PlayerInventory", "gold")
local success, currentGold = pcall(function()
return inventoryStore:GetAsync("User_1234")
end)
Managing a Data Store
Setting Data
A data store is essentially a dictionary, similar to a Lua table. A unique key indexes each value in the data store, such as a player's unique Player.UserId or a named string for a game promo.
Key | Value |
---|---|
31250608 | 50 |
351675979 | 20 |
505306092 | 78000 |
Player Data |
Key | Value |
---|---|
ActiveSpecialEvent | SummerParty2 |
ActivePromoCode | BONUS123 |
CanAccessPartyPlace | true |
Promo Data |
To create a new entry, call SetAsync() with the key name and a value.
local DataStoreService = game:GetService("DataStoreService")
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
local success, errorMessage = pcall(function()
experienceStore:SetAsync("User_1234", 50)
end)
if not success then
print(errorMessage)
end
Reading Data
The GetAsync() function reads the value of a data store entry. It requires just the key name of the entry.
local DataStoreService = game:GetService("DataStoreService")
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
local success, currentExperience = pcall(function()
return experienceStore:GetAsync("User_1234")
end)
if success then
print(currentExperience)
end
Incrementing Data
IncrementAsync() changes a numerical value in a data store. This function requires the key name of the entry and a number indicating how much to change the value.
local DataStoreService = game:GetService("DataStoreService")
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
local success, newExperience = pcall(function()
return experienceStore:IncrementAsync("Player_1234", 1)
end)
if success then
print(newExperience)
end
Updating Data
UpdateAsync() changes any stored value in a data store. This function requires the key name of the entry plus a callback function which defines how the entry should be updated. This callback takes the current value and returns the new value, based on whatever logic you define. If the callback returns nil, the write operation is cancelled and the value remains unchanged.
local DataStoreService = game:GetService("DataStoreService")
local nicknameStore = DataStoreService:GetDataStore("Nicknames")
local function makeNameUpper(currentName)
local nameUpper = string.upper(currentName)
return nameUpper
end
local success, updatedName = pcall(function()
return nicknameStore:UpdateAsync("User_1234", makeNameUpper)
end)
if success then
print("Uppercase Name:", updatedName)
end
Set vs. Update
SetAsync() is best for a quick update of a specific key, and it only counts against the write limit. However, it may cause data inconsistency if two servers attempt to set the same key at the same time.
UpdateAsync() is safer for handling multi-server attempts because it reads the current key value from the server that last updated it before making any changes. However, it's somewhat slower because it reads before it writes, and it also counts against both the read and write limit.
Removing Data
RemoveAsync() removes an entry and returns the value that associates with the key.
local DataStoreService = game:GetService("DataStoreService")
local nicknameStore = DataStoreService:GetDataStore("Nicknames")
local success, removedValue = pcall(function()
return nicknameStore:RemoveAsync("User_1234")
end)
if success then
print(removedValue)
end
Ordered Data Stores
By default, data stores do not sort their content, but sometimes it's necessary to get data in an ordered fashion, such as persistent leaderboard stats. You can achieve this by calling GetOrderedDataStore() instead of GetDataStore().
local DataStoreService = game:GetService("DataStoreService")local characterAgeStore = DataStoreService:GetOrderedDataStore("CharacterAges")
Ordered data stores support the same basic functions as default data stores, plus the unique OrderedDataStore:GetSortedAsync() function. This retrieves multiple sorted keys based on a specific sorting order, page size, and minimum/maximum values.
The following example sorts character data into pages with three entries each in descending order, then loops through the pages and outputs each character's name/age.
local DataStoreService = game:GetService("DataStoreService")
local characterAgeStore = DataStoreService:GetOrderedDataStore("CharacterAges")
-- Populate ordered data store
local characters = {
Mars = 19,
Janus = 20,
Diana = 18,
Venus = 25,
Neptune = 62
}
for char, age in pairs(characters) do
local success, errorMessage = pcall(function()
characterAgeStore:SetAsync(char, age)
end)
if not success then
print(errorMessage)
end
end
-- Sort data by descending order into pages of three entries each
local success, pages = pcall(function()
return characterAgeStore:GetSortedAsync(false, 3)
end)
if success then
while true do
-- Get the current (first) page
local entries = pages:GetCurrentPage()
-- Iterate through all key-value pairs on page
for _, entry in pairs(data) do
print(entry.key .. " : " .. tostring(entry.value))
end
-- Check if last page has been reached
if pages.IsFinished then
break
else
print("----------")
-- Advance to next page
pages:AdvanceToNextPageAsync()
end
end
end
Metadata
There are two types of metadata associated with keys:
- Service-defined - Every object has default read-only metadata such as the most recent update time and creation time.
- User-defined - Through the DataStoreSetOptions object and its SetMetadata() function, you can include custom metadata for tagging and categorization.
Metadata is managed by expanding the SetAsync(), GetAsync(), UpdateAsync(), IncrementAsync(), and RemoveAsync() functions.
SetAsync() accepts optional third and fourth arguments:
Table of UserIds, highly recommended to assist with content copyright and intellectual property tracking/removal.
A DataStoreSetOptions object on which you can define custom metadata using its SetMetadata() function.
local DataStoreService = game:GetService("DataStoreService")local experienceStore = DataStoreService:GetDataStore("PlayerExperience")local setOptions = Instance.new("DataStoreSetOptions")setOptions:SetMetadata({["ExperienceElement"] = "Fire"})local success, errorMessage = pcall(function()experienceStore:SetAsync("User_1234", 50, {1234}, setOptions)end)if not success thenprint(errorMessage)end
GetAsync(), IncrementAsync(), and RemoveAsync() return a second value (DataStoreKeyInfo object) that contains both service-defined properties and functions to fetch user-defined metadata:
- DataStoreKeyInfo:GetUserIds() — Fetches the table of UserIds that was passed to SetAsync().
- DataStoreKeyInfo:GetMetadata() — Fetches user-defined metadata that was passed to SetAsync() through SetMetadata().
- DataStoreKeyInfo.Version — Version of the key.
- DataStoreKeyInfo.CreatedTime — Time the key was created, formatted as the number of milliseconds since epoch.
- DataStoreKeyInfo.UpdatedTime — Last time the key was updated, formatted as the number of milliseconds since epoch.
local DataStoreService = game:GetService("DataStoreService")local experienceStore = DataStoreService:GetDataStore("PlayerExperience")local success, currentExperience, keyInfo = pcall(function()return experienceStore:GetAsync("User_1234")end)if success thenprint(currentExperience)print(keyInfo.Version)print(keyInfo.CreatedTime)print(keyInfo.UpdatedTime)print(keyInfo:GetUserIds())print(keyInfo:GetMetadata())endThe callback function of UpdateAsync() takes an additional parameter (DataStoreKeyInfo object) that describes the current key state. It returns the modified value, the key's associated UserIds, and the key's metadata.
local DataStoreService = game:GetService("DataStoreService")local nicknameStore = DataStoreService:GetDataStore("Nicknames")local function makeNameUpper(currentName, keyInfo)local nameUpper = string.upper(currentName)local userIDs = keyInfo:GetUserIds()local metadata = keyInfo:GetMetadata()return nameUpper, userIDs, metadataendlocal success, updatedName, keyInfo = pcall(function()return nicknameStore:UpdateAsync("User_1234", makeNameUpper)end)if success thenprint(updatedName)print(keyInfo.Version)print(keyInfo.CreatedTime)print(keyInfo.UpdatedTime)print(keyInfo:GetUserIds())print(keyInfo:GetMetadata())end
User-defined metadata has the following limits:
- Key length: up to 50 characters.
- Value length: up to 250 characters.
- No limit for the total number of key-value pairs but the total size cannot exceed 300 characters.
Versioning
With versioning, SetAsync() and UpdateAsync() create new versions instead of overwriting existing data, and GetAsync() reads the latest version. DataStoreService periodically checks the timestamps of each version and removes versions older than 30 days, but retains the latest version indefinitely.
There are three new APIs for versioning operations:
Function | Description |
---|---|
ListVersionsAsync() | Lists all versions for a key by returning a DataStoreVersionPages instance that you can use to enumerate all version numbers. You can filter versions based on a time range as shown in the code example below. |
GetVersionAsync() | Retrieves a specific version of a key using its version number. |
RemoveVersionAsync() | Deletes a specific version of a key. |
Versioning is convenient for user support. For example, if a user reports that a problem occurred at 2020-10-09T01:42, you can revert to a previous version using the code below.
local DataStoreService = game:GetService("DataStoreService")
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")
local DATA_STORE_KEY = "User_1234"
local maxDate = DateTime.fromUniversalTime(2020, 10, 09, 01, 42)
-- Get the version closest to the given time
local listSuccess, pages = pcall(function()
return experienceStore:ListVersionsAsync(DATA_STORE_KEY, Enum.SortDirection.Descending, nil, maxDate.UnixTimestampMillis)
end)
if listSuccess then
local items = pages:GetCurrentPage()
if table.getn(items) > 0 then
-- Read the closest version
local closestEntry = items[1]
local success, value, info = pcall(function()
return experienceStore:GetVersionAsync(DATA_STORE_KEY, closestEntry.Version)
end)
-- Restore current value by overwriting with the closest version
if success then
local setOptions = Instance.new("DataStoreSetOptions")
setOptions:SetMetadata(info:GetMetadata())
experienceStore:SetAsync(DATA_STORE_KEY, value, nil, setOptions)
end
else
-- No entries found
end
end
Listing and Prefixes
Data stores allow for listing by prefix (the first n characters of a name, such as "d", "do", or "dog" for any key or data store with a prefix of "dog").
You can specify a prefix when listing all data stores or keys, and only objects matching that prefix will be returned. Both functions return a DataStoreListingPages object that you can use to enumerate the list.
Function | Description |
---|---|
ListDataStoresAsync() | Lists all data stores. |
ListKeysAsync() | Lists all keys in a data store. |
AllScopes Property
DataStoreOptions contains an AllScopes property that allows you to return keys from all scopes in a convenient list. You can then use a list item's KeyName property for common data store operations like GetAsync() and RemoveAsync(). When you use this property, the second parameter of GetDataStore() must be an empty string ("").
local DataStoreService = game:GetService("DataStoreService")local options = Instance.new("DataStoreOptions")options.AllScopes = truelocal ds = DataStoreService:GetDataStore("DS1", "", options)
If you enable the AllScopes property and create a new key in the data store, you must always specify a scope for that key in the format of scope/keyname, otherwise the APIs throw an error. For example, gold/player_34545 is acceptable with gold as the scope, but player_34545 leads to an error.
Consider the following data set:
global/K1 | house/K1 |
global/L2 | house/L2 |
global/M3 | house/M3 |
Error Codes
Requests to data stores may occasionally fail due to poor connectivity or other issues. Wrapping data store functions in pcall() can handle any errors and return a message with an error code.
Error Code Reference
Error Code | Error Name | Error Message | Notes |
---|---|---|---|
101 | KeyNameEmpty | Key name can't be empty. | Check if the key input into the data store function is an empty string. |
102 | KeyNameLimit | Key name exceeds the 50 character limit. | Check if the key input into the data store function exceeds a length of 50. |
103 | ValueNotAllowed | X is not allowed in DataStore. | An invalid value of type X was returned by a bad update function. |
104 | CantStoreValue | Cannot store X in DataStore. | A value of type X returned by the update function did not serialize. |
105 | ValueTooLarge | Serialized value exceeds X limit. | If you're setting a value with SetAsync() or UpdateAsync(), the serialized length of the value cannot exceed the size X. The serialized length of the data can be checked with JSONEncode(). |
106 | MaxValueInvalid | MaxValue and MinValue must be integers. | If you're passing a maximum value to GetSortedAsync() for an OrderedDataStore, it must be an integer. |
106 | MinValueInvalid | MaxValue and MinValue must be integers. | If you're passing a minimum value to GetSortedAsync() for an OrderedDataStore, it must be an integer. |
106 | PageSizeGreater | PageSize must be within a predefined range. | The minimum page size for an OrderedDataStore is 1. |
106 | PageSizeLesser | PageSize must be within a predefined range. | The maximum page size for an OrderedDataStore is 100. |
301 | GetAsyncThrottle | GetAsync request dropped. Request was throttled, but throttled request queue was full. | GetAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
302 | SetAsyncThrottle | SetAsync request dropped. Request was throttled, but throttled request queue was full. | SetAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
303 | IncreAsyncThrottle | IncrementAsync request dropped. Request was throttled, but throttled request queue was full. | IncrementAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
304 | UpdateAsyncThrottle | UpdateAsync request dropped. Request was throttled, but throttled request queue was full. | UpdateAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
304 | TransformThrottle | UpdateAsync request dropped. Request was throttled, but throttled request queue was full. | UpdateAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
305 | GetSortedThrottle | GetSorted request dropped. Request was throttled, but throttled request queue was full. | GetSortedAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
306 | RemoveAsyncThrottle | RemoveAsync request dropped. Request was throttled, but throttled request queue was full. | RemoveAsync() request has exceeded the maximum queue size and Roblox is unable to process the requests at the current throughput. |
401 | DataModelNoAccess | Request Failed. DataModel Inaccessible when the game is shutting down. | DataModel is uninitialized because the game is shutting down. |
402 | LuaWebSrvsNoAccess | Request Failed. LuaWebService Inaccessible when the game is shutting down. | LuaWebService is uninitialized because the game is shutting down. |
403 | StudioAccessToApisNotAllowed | Cannot write to DataStore from Studio if API access is not enabled. | API access must be active in order to use Data Stores in Studio. |
404 | InternalError | OrderedDataStore does not exist. | The OrderedDataStore associated with this request was not found. This may be a sign of data corruption, so you may want to retry the request at a later time. |
501 | InternalError | Can't parse response, data may be corrupted. | The server was unable to parse the response to your request. This may be a sign of data corruption, so you may want to retry the request at a later time. |
502 | RequestRejected | API Services rejected request with error: X. | Error X occurred when processing on Roblox servers. Depending on the response, you may want to retry the request at a later time. |
503 | InternalError | Data store request successful, but key not found. | The key requested was not found in the DataStore. This may be a sign of data corruption, so you may want to retry the request at a later time. |
504 | InternalError | Data store request successful, but response not formatted correctly. | The server was unable to parse the response to your request. This may be a sign of data corruption, so you may want to retry the request at a later time. |
505 | InternalError | OrderedDataStore request successful, but response not formatted correctly. | The server was unable to parse the response to your OrderedDataStore request. This may be a sign of data corruption, so you may want to retry the request at a later time. |
511 | AttributeSizeTooLarge | Metadata attribute size exceeds X limit. | The serialized metadata size exceeds the limit of X. The value X is dynamic, if the size changes, the value also changes. |
512 | UserIdLimitExceeded | UserID size exceeds limit of X. | The length of the user IDs array provided by the user exceeded the limit of X. |
513 | AttributeFormatError | Attribute userId format is invalid. | The user ID provided is not a number. |
513 | AttributeFormatError | Attribute metadata format is invalid. | The metadata is not a table. |
Limits
There are also limits applied to the data store model. If an experience exceeds these limits, the service automatically throttles the experience's data store usage, causing requests to be placed in a queue.
When a limit is reached, further requests are placed into one of four queues: set, ordered set, get, and ordered get. Requests in a queue are handled in the order they are received and the called function continues to yield as long as its request is queued. If the data store key itself is throttled, the request is temporarily skipped but still in the queue. Each queue has a limit of 30 requests and, when this limit is exceeded, the requests fail with an error code in the 301–306 range indicating that the request was dropped entirely.
Server Limits
Each server is allowed a certain number of data store requests based on the request type and number of players (more data is needed for more players).
Request Type | Functions | Requests per Minute |
---|---|---|
Get | GetAsync() | 60 + numPlayers × 10 |
Set (limit is shared among all listed functions) | SetAsync() IncrementAsync() UpdateAsync() RemoveAsync() | 60 + numPlayers × 10 |
Get Sorted | GetSortedAsync() | 5 + numPlayers × 2 |
Get Version | GetVersionAsync() | 5 + numPlayers × 2 |
List | ListDataStoresAsync() ListKeysAsync() ListVersionAsync() | 5 + numPlayers × 2 |
Remove | RemoveVersionAsync() | 5 + numPlayers × 2 |
Data Limits
Along with request frequency, data stores limit how much data can be used per entry. The data store name, key name, and scope must all be under a certain character length, as well as the amount of data stored.
Component | Maximum Number of Characters |
---|---|
Data Store Name | 50 |
Key Name | 50 |
Scope | 50 |
Data (Key Value) | 4,194,304 per key |
Throughput Limits
The following table describes per-key throughput limits to ensure optimal performance on Roblox servers.
Request Type | Limit |
---|---|
Read | 25 MB per minute |
Write | 4 MB per minute |
Caching
Keys cache locally for 4 seconds after the first read. A GetAsync() call within these 4 seconds returns a value from the cache. Modifications to the key by SetAsync() or UpdateAsync() apply to the cache immediately and restart the 4 second timer.
GetVersionAsync(), ListVersionsAsync(), ListKeysAsync(), and ListDataStoresAsync() don't implement caching and always fetch the latest data from the service.
Best Practices
The following guidelines help you manage your data more efficiently and take advantage of future improvements.
Create Fewer Data Stores
Data stores behave similarly to tables in databases. Minimize the number of data stores in an experience and put related data in each data store. This approach allows you to configure each data store individually (versioning and indexing/querying) to improve the service's efficiency to operate the data. As more features become available, this approach also allows you to more easily manage data stores.
Use a Single Object for Related Data
Since the increased maximum object size is 4 MB, there should be enough space to follow this rule. You can fetch all relevant data in one call and use the quota (limit) more efficiently. Additionally, SetAsync() updates all data, so all data for the same player is always in sync. The versioning system versions individual objects rather than the whole data store. When restoring to older versions, self-contained objects are now consistent and useful.
Use Key Prefixes to Organize Your Data
You can filter keys with a certain prefix when calling ListKeysAsync(). Consider an experience that supports players having multiple character profiles. You can save keys with a prefix such as /User_1234/profiles/warrior and /User_1234/profiles/mage and use a prefix search with /User_1234/profiles to get a list of all profiles.