Writing to and Reading from Data Stores in Roblox
Data Stores are the built-in persistence system Roblox provides. They let you save and retrieve information tied to a player or the entire game, and they work across sessions. Most tutorials show you SetAsync and GetAsync and call it a day. That works until something breaks, which is usually when your game launches and gets enough players to hit edge cases nobody thought about. The service is accessed through DataStoreService from ServerScriptService. You create a DataStore instance with GetDataStore, passing a string name. The name becomes the logical key for your bucket of data. Everything else flows from there. The two functions you will use most are GetAsync for reading and UpdateAsync for writing. SetAsync is fine for simple cases, but it blindly overwrites whatever was there before without giving you a chance to see what changed. UpdateAsync returns the previous value, lets you calculate a new one, and handles conflicts atomically. That atomicity matters when two requests try to write the same key at the same time.
Here is a minimal reading pattern.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData-v4")
local function loadPlayer(player)
local success, data = pcall(function()
return store:GetAsync("player_" .. player.UserId)
end)
if not success then
warn("Failed to load data: " .. tostring(data))
return nil
end
return data or {}
end
And a basic write using UpdateAsync. The pcall wrapping is not optional advice. DataStore calls can fail for reasons outside your control. Network blips, Roblox backend timeouts, throttling. If you do not wrap them in error handling, a single bad request can crash your saving thread and leave a player with nothing. Roblox imposes strict request limits on Data Stores. The exact numbers shift over time, but you should assume a small number of requests per second per DataStore instance and a hard cap on total requests per minute across the server. Exceed those limits and Roblox throttles you. Throttled requests do not just queue. They fail with an error that looks identical to a network failure, and your code needs to handle both the same way.
Get the Full Details

I ran into this head-on when my survival game hit about eight hundred concurrent players during a test event. Every player was saving on a ten-second timer, plus on join and on leave. That meant roughly 160 DataStore requests per second, all hammering the same instance. The server started spitting out errors constantly, and I watched players lose progress between saves because the throttled writes silently failed. I had already wrapped everything in pcall, but the pcall was catching the failure and moving on as if nothing happened. The data was gone. The fix was threefold. First, I reduced the save frequency. Instead of ten-second timers, I switched to a dirty-tracking system where data only saved when something actually changed, and I batched the saves into a single request per player per tick rather than one request per stat.
Second, I split the data across multiple DataStore instances. One for inventory, one for progression, one for preferences. Each instance gets its own rate limit allocation, so spreading the load across five stores effectively multiplies your throughput by five. Third, I implemented retry logic with exponential backoff. A failed request gets retried once after a short delay, twice after a longer delay, and then it is logged and dropped. Most throttle failures resolve within two seconds. A few do not, and those are the ones you want to log so you can investigate.
local function safeUpdateAsync(store, key, updateFunction, retries)
retries = retries or 3
local lastError
for i = 1, retries do
local success, result = pcall(function()
return store:UpdateAsync(key, updateFunction)
end)
if success then
return true, result
end
lastError = result
wait(math.min(2 ^ i, 10))
end
warn("All retries exhausted for key: " .. key .. " Error: " .. tostring(lastError))
return false, lastError
end
OrderedDataStore for leaderboards and ranked saves
When you need a global ranking, Roblox provides OrderedDataStore. It is separate from regular DataStore and works differently. You set a numeric value associated with a key, and you query rankings with GetSortedAsync. This is how you build top-players tables, weekly leaderboards, or any system where order matters. The thing people miss is that GetSortedAsync is expensive. Each call can trigger a scan across the entire dataset for that store. Calling it every time a leaderboard refreshes will burn through your request quota fast. Cache the results server-side and refresh on a schedule or on trigger events instead of querying every time a client asks for the rank. Another trap: OrderedDataStore does not guarantee stable ordering when scores are tied. Two players with the same score can swap positions between reads. If your game uses score ties for anything meaningful, you need a secondary sort key, usually the UserId, baked into your comparison logic before you write.

Server-only rule and common mistakes
DataStore can only be accessed from the server. Client scripts that try to call DataStoreService will throw an error. This sounds obvious until you have a developer who puts a save function in a LocalScript because it is easier to debug, then spends an hour wondering why it fails in published games. Another mistake is tying the DataStore key to the player's name or a changing identifier instead of their UserId. Usernames can change. Player names can be reused or modified. UserId never changes for a given account. Always use UserId as the primary key component. A third one: not handling data format migration. Your game launches, you ship version one with a data structure that stores coins as a flat number. Three months later you add a new currency and need to store coins as {amount = 500, type = "gold"}. Old players will load raw numbers, not tables. Your code needs to detect the old format and convert it on read, or you will get errors whenever someone tries to index a number like it was a table.
Quota limits and when Data Store stops working
There are hard quotas on DataStore usage that go beyond rate limits. Each DataStore has a maximum amount of data you can store, and there are limits on the total number of keys. For a typical game, these numbers are generous. They become a problem when you start storing large blobs, like entire player inventories encoded as JSON strings, in a single key. If a player accumulates thousands of items, that serialized string can grow large enough to hit size limits or slow down reads significantly. I learned this the hard way with a trading system. Players could accumulate rare items over months, and the inventory data grew to around eighty kilobytes per player. Reads that used to take milliseconds started taking hundreds of milliseconds, and occasional quota warnings appeared in the console. The solution was splitting the inventory into chunks. One DataStore key for metadata, another for item list, and a third for trade history. Smaller reads, lower latency, and enough headroom that quota warnings disappeared.
What Data Store cannot do well
DataStore is not designed for real-time multiplayer state. If you need players to see each other's positions, health, or active effects, use ReplicatedStorage and RemoteEvents, or consider a dedicated networking approach. DataStore adds latency, has no push mechanism, and is not built for sub-second consistency. It is also not a substitute for proper backup strategies. Roblox retains your data, but if you accidentally overwrite a save with corrupted data, there is no undo button inside the system. I keep a daily export routine that writes player data to a secondary store and logs the keys to a text file in the game. Not perfect, but it caught a bug where a malformed table was being saved and would have wiped dozens of accounts without any warning.

Data Store Roblox edge cases you will hit
One edge case that is easy to overlook involves player rejoin during a save. If a player leaves, their data is saved, then they immediately rejoin while the save is still in flight, the new session might read stale data or trigger a write conflict. The safe approach is to debounce saves per player and queue them rather than firing every time a change happens. A simple cooldown of half a second between saves per player cut my request volume by about forty percent without making the game feel sluggish. Another one: test mode vs published mode. DataStore behavior can differ between Studio play and published games. Studio has relaxed limits and different error handling. Code that works perfectly in Studio can fail in a live game. Test with published builds, or use a service like Roblox Cloud Testing, before you assume everything is solid. DataStore works fine when you respect its limits and plan for failures. It breaks when you treat it like a database that will always respond instantly. Write defensively, spread your data across multiple stores, cache what you can, and keep your saves event-driven instead of timer-driven. Those habits will save you more trouble than any advanced feature Roblox adds later.