Skip to content
Back to skills

Roblox Data Stores

ASecurity

Persistence and cross-server data - DataStoreService (UpdateAsync, retries, budgets, limits), session locking with ProfileStore, schema migrations, OrderedDataStore leaderboards, MemoryStore queues/sorted maps/hash maps, MessagingService, right-to-be-forgotten. Use when saving player data, building leaderboards or queues, or fixing data loss, duplication, or throttling.

  • 5 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 29, 2026
ai-agentsgotestingapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add EL4CTEO/roblox-skills --skill roblox-data-stores --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Roblox Data Stores?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Roblox Data Stores
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/el4cteo-roblox-data-stores/badge)](https://www.skillsdirectory.com/skills/el4cteo-roblox-data-stores)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: roblox-data-stores
description: Persistence and cross-server data - DataStoreService (UpdateAsync, retries, budgets, limits), session locking with ProfileStore, schema migrations, OrderedDataStore leaderboards, MemoryStore queues/sorted maps/hash maps, MessagingService, right-to-be-forgotten. Use when saving player data, building leaderboards or queues, or fixing data loss, duplication, or throttling.
---

# Roblox data stores and cloud services

## Choose the service

| Need | Service | Notes |
| --- | --- | --- |
| Player progress, inventory, settings | **DataStore** via ProfileStore (session-locked) | One key per player (`User_{UserId}`), ≤ 4 MB. |
| Global leaderboards | `OrderedDataStore` | Integer values only; periodic updates, cached reads. |
| Fast, temporary, cross-server state | `MemoryStoreService` | Sorted maps, queues, hash maps; everything expires (TTL ≤ 45 days). |
| Matchmaking queues, live events, server lists | MemoryStore queue / sorted map | Not durable — never the only copy of valuable data. |
| Broadcast to all servers | `MessagingService` | ≤ 1 kB per message, best effort, no delivery guarantee. |
| Values changed by live-ops without publishing | Experience configs (`ConfigService`) | See `roblox-analytics-liveops`. |

## Player data: use session locking

Two servers must never own the same player's data at once (teleports, fast rejoins), or items duplicate
and progress is lost. **ProfileStore** (loleris) implements session locking, auto-save, and
`BindToClose` flushing, and is the recommended default. Install it from Wally/the Creator Store and put
it in `ServerScriptService`.

```luau
-- ServerScriptService/Server/Services/DataService.luau (ProfileStore)
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")

local ProfileStore = require(ServerScriptService.ServerPackages.ProfileStore)

local TEMPLATE = {
	version = 1,
	coins = 0,
	inventory = {} :: { [string]: number },
}
type PlayerData = typeof(TEMPLATE)

local PlayerStore = ProfileStore.New("PlayerData", TEMPLATE)
local profiles: { [Player]: typeof(PlayerStore:StartSessionAsync("")) } = {}

local function onPlayerAdded(player: Player)
	local profile = PlayerStore:StartSessionAsync(`User_{player.UserId}`, {
		Cancel = function()
			return player.Parent ~= Players -- stop waiting if the player left
		end,
	})
	if profile == nil then
		player:Kick("Could not load your data. Please rejoin.")
		return
	end
	profile:AddUserId(player.UserId) -- GDPR / right-to-be-forgotten linkage
	profile:Reconcile() -- fill in fields added to TEMPLATE since this profile was created
	profile.OnSessionEnd:Connect(function()
		profiles[player] = nil
		player:Kick("Your data was loaded on another server. Please rejoin.")
	end)
	if player.Parent == Players then
		profiles[player] = profile
	else
		profile:EndSession() -- left while loading
	end
end

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(function(player)
	local profile = profiles[player]
	if profile then
		profile:EndSession() -- saves and releases the lock
	end
end)

local DataService = {}
function DataService.get(player: Player): PlayerData?
	local profile = profiles[player]
	return profile and profile.Data
end
return DataService
```

Mutate `profile.Data` directly on the server (it is cached in memory and auto-saved). Don't
`SetAsync` per change. Check the ProfileStore docs for the exact current API before relying on less
common features (global updates, `MessageAsync`, mock stores for Studio).

## Raw DataStoreService rules (when not using a wrapper)

- **Buffer in memory**: load once on join, mutate the in-memory table, save periodically (e.g. every
  2–5 minutes with random jitter), on leave, at checkpoints (purchases), and in `game:BindToClose`.
- **`UpdateAsync` over `SetAsync`** whenever the write depends on the old value or multiple servers may
  write the key. The transform function must be pure and fast (no yields); return `nil` to cancel.
- **Retry transient errors** with exponential backoff + jitter, capped attempts; process retries for
  a key in order (an old retry must not overwrite newer data). Treat a failed write as "outcome unknown".
- Never save `Instance`s, `Vector3`, `CFrame`, functions, or mixed/sparse tables: store plain tables,
  numbers, strings, booleans, and `buffer`s. Serialize datatypes (`{ x, y, z }`). Validate UTF-8 strings.
- Keys and data store names ≤ 50 characters; value ≤ 4,194,304 characters after JSON encoding.
- Stable key patterns (`User_{UserId}`), never display names. Few data stores, few keys per player.
- Studio needs **File → Experience Settings → Security → Enable Studio Access to API Services**; use a separate test
  universe or a different data store name so testing never touches production data.

Complete raw implementation (session lock, autosave, BindToClose, retries):
[references/raw-datastore.md](references/raw-datastore.md).

## Budgets and limits (2026)

| Limit | Value |
| --- | --- |
| Experience-wide standard reads / writes | `300 + 40 × CCU` / `300 + 20 × CCU` per minute (shared with Open Cloud) |
| Default per-server reads/writes | `60 + 40 × players` per minute each (configurable) |
| Ordered writes per server | `30 + 5 × players` per minute |
| Per-key throughput | 25 MB/min read, 4 MB/min write |
| Storage (latest versions, compressed) | `500 MB + 1 MB × lifetime users` per experience |
| Queue per request type | 30 pending requests, then errors 301–306 |

- `UpdateAsync` consumes both read and write budget.
- Check `DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite)` before
  bulk work; tune per-server limits once at startup with `SetRateLimitForRequestType`.
- Don't pre-compress data; Roblox compresses automatically. Shard only genuinely hot keys.
- Use versions (`ListVersionsAsync`, `GetVersionAtTimeAsync`) to restore data instead of writing backup keys.

## Schema versioning and migrations

Store a `version` field. On load, run ordered migration functions until the data reaches the current
version, then reconcile defaults. Never delete fields in the same release that stops writing them;
never reuse field names with different meanings.

```luau
--!strict
type Data = { [string]: any }

local MIGRATIONS: { (Data) -> () } = {
	[1] = function(data) -- v1 -> v2: "gold" renamed to "coins"
		data.coins = data.gold or 0
		data.gold = nil
	end,
	[2] = function(data) -- v2 -> v3: inventory list -> counts
		local counts: { [string]: number } = {}
		for _, itemId in data.inventory or {} do
			counts[itemId] = (counts[itemId] or 0) + 1
		end
		data.inventory = counts
	end,
}
local CURRENT_VERSION = #MIGRATIONS + 1

local function migrate(data: Data): Data
	local version = data.version or 1
	while version < CURRENT_VERSION do
		MIGRATIONS[version](data)
		version += 1
	end
	data.version = CURRENT_VERSION
	return data
end

print(migrate({ gold = 5, inventory = { "Sword", "Sword" } }))
```

## Leaderboards (OrderedDataStore)

- Values must be integers. Write on a schedule or on meaningful change, not every increment.
- Read top N with `GetSortedAsync(false, 100)` on **one** server loop (e.g. every 60 s), cache the
  result, and replicate to clients. Don't let every server/client read constantly.
- For "this week" boards, include the period in the store name (`Wins_2026W39`) or use a MemoryStore sorted map.

## MemoryStore essentials

- Quotas are experience-wide: memory `64 KB + 1.2 KB × users`; requests `1000 + 120 × CCU` units/min.
- `MemoryStoreSortedMap`: ranked data, server browsers, live leaderboards (`SetAsync` with expiration,
  `GetRangeAsync`, `UpdateAsync`).
- `MemoryStoreQueue`: matchmaking/work queues (`AddAsync`, `ReadAsync` + `RemoveAsync` with the returned id
  after successful processing, invisibility timeout for crash safety).
- `MemoryStoreHashMap`: high-volume key-value without sorting (`SetAsync`, `GetAsync`, `UpdateAsync`, `ListItemsAsync`).
- Always set expirations; handle throttling (`pcall` + retry); keep items small.

## MessagingService essentials

- Message ≤ 1 kB. Per server: `600 + 240 × players` sends/min; `20 + 8 × players` subscriptions.
- Delivery is **best effort** and not ordered: use it for notifications ("refresh your cache",
  global announcements), never as the source of truth. Pair with DataStore/MemoryStore for state.
- Wrap `SubscribeAsync`/`PublishAsync` in `pcall`; they can fail or yield.

More code (MemoryStore queue matchmaking, cross-server announcement, RTBF): [references/memory-and-messaging.md](references/memory-and-messaging.md).

## Right to be forgotten

Use static key patterns so **automated RTBF** (configured in Creator Dashboard) can delete player data
templates like `PlayerData/User_{userId}`. Otherwise handle the right-to-erasure webhook (see
`roblox-open-cloud`). Link keys to user IDs: `SetAsync(key, value, { userId })`, return them as the second value from an
`UpdateAsync` transform, or call ProfileStore's `AddUserId`.

## Related skills

`roblox-security` (duplication and trade exploits), `roblox-monetization` (receipts),
`roblox-open-cloud` (external access, bulk operations), `roblox-teleport-matchmaking`.

Files in this skill

  • SKILL.md9.1 KB
  • references/memory-and-messaging.md3.9 KB
  • references/raw-datastore.md4.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.