roblox-datastores · git:20260919.e20f73c · 2026-09-19 · sha256 f0f9c6f6bfbcc5bb

roblox-datastores git:20260919.e20f73cA

Immutable. This exact content never changes and is served at /api/v1/blob/f0f9c6f6bfbcc5bb.

---
name: roblox-datastores
description: "Implement or debug persistent Roblox player data, leaderboards, migrations, and deletion requests. Use for concurrent writes, session locking, recovery, caching, quotas, or choosing DataStores versus MemoryStores."
last_reviewed: 2026-08-18
---

# roblox-datastores

Keep persistence on the server. For concurrent writes, use a pure `UpdateAsync` transform; after a write error, reconcile the backend state before retrying or compensating.

**Official sources:**
- https://create.roblox.com/docs/cloud-services/data-stores
- https://create.roblox.com/docs/cloud-services/data-stores-vs-memory-stores
- https://create.roblox.com/docs/cloud-services/data-stores/versioning-listing-and-caching
- https://create.roblox.com/docs/cloud-services/data-stores/error-codes-and-limits
- https://create.roblox.com/docs/cloud-services/data-stores/best-practices
- https://create.roblox.com/docs/cloud-services/data-stores/data-stores-manager
- https://create.roblox.com/docs/cloud-services/data-stores/observability
- Engine classes: DataStoreService, GlobalDataStore, DataStore, OrderedDataStore, DataStoreOptions, DataStoreKeyInfo, DataStoreSetOptions, etc. (full reference at https://create.roblox.com/docs/reference/engine)

For temporary queues, lobbies, and caches, use MemoryStoreService. Read [types-of-datastores.md](references/types-of-datastores.md) when choosing a store.

Cross-reference:
- [roblox-networking/SKILL.md](../roblox-networking/SKILL.md) for server-authoritative validation before any write.
- [roblox-core/SKILL.md](../roblox-core/SKILL.md) for service acquisition patterns, pcall discipline, and Luau serialization rules.
- [roblox-gamepasses/SKILL.md](../roblox-gamepasses/SKILL.md) when purchases affect persistent state (re-verify ownership on load).
- [roblox/SKILL.md](../roblox/SKILL.md) for the overall hub and Engine API link.

## Choose a store

1. Is the data temporary/ephemeral, high-churn, or only needed while servers are active (queues, lobbies, rate counters, short caches)?  
   → Use **MemoryStoreService** (queues, sorted maps, etc.). DataStores are overkill and more expensive/slower. See references and official comparison.

2. Do you need sorted numeric queries for leaderboards or rankings?  
   → Use **OrderedDataStore** (via GetOrderedDataStore). Values **must** be integers. No versioning/metadata. Special GetSortedAsync + DataStorePages iteration. Limits differ (see references/limits...).

3. Do you need versioning for recovery/audit, user-defined metadata, key listing, or the richer DataStore API (`ListKeysAsync`, `ListVersionsAsync`, `GetVersionAsync`, `GetVersionAtTimeAsync`)?
   → Use the full **DataStore** created via `GetDataStore` with `DataStoreOptions` (this returns the modern DataStore class). Preferred for serious player data.

4. Simple key-value persistent storage without the above needs?  
   → Use `DataStoreService:GetDataStore()` (returns a standard DataStore) or `DataStoreService:GetGlobalDataStore()` (legacy global store, scope `"u"`). For new work, prefer a named `GetDataStore` store.

**Scopes vs prefixes (modern recommendation):** For new work, prefer key prefixes (e.g. "profiles/User_1234") + ListKeysAsync filtering over legacy scopes. Scopes prepend to keys automatically. Use DataStoreOptions.AllScopes=true + empty scope string for cross-scope listing (see references).

**Studio access:** If Studio needs backend APIs, use a dedicated test experience. Do not enable Studio sessions to read or write production data.

## Recommended safety workflows

### Player data profile pattern (most common)

See references/best-practices-and-gotchas.md for full examples and variations (including session locking and profile patterns).

High-level server flow (in a ModuleScript required by ServerScriptService scripts):

1. On PlayerAdded:
   - Load with pcall + GetAsync (consider UseCache=false for critical fresh read after potential previous server issues).
   - Merge with defaults.
   - Store in a server-side table or profile object (e.g. `playerData[player.UserId] = data`).
   - Apply any gamepass perks (cross-ref monetization skill) or other state.
   - Optionally fire a Remote to client for initial UI sync (never trust client data).

2. During play:
   - Mutate the in-memory profile.
   - Periodically auto-save (every 30-60s) using UpdateAsync or SetAsync with proper transform.
   - On important events (purchase, level up, trade), save immediately with UpdateAsync for safety.

3. On PlayerRemoving / BindToClose / character death:
   - Final save with pcall.
   - Clean up in-memory data.
   - For BindToClose, yield up to ~30 seconds to finish final saves (Roblox gives a bounded grace period).

4. On errors during write:
   - Use the verification pattern: after a failed write, immediately GetAsync with UseCache=false to see actual backend state before deciding retry/refund/rollback.

Use **UpdateAsync** with a pure (non-yielding) transform function for any value that can be concurrently modified by multiple servers. It reads-then-writes atomically from the perspective of the last writer.

**Serialization rules (critical):** Only nil, booleans, numbers (no inf/-inf/nan), strings (valid UTF-8), buffer, and tables of the above. No functions, no metatables on saved tables, no cycles. Use HttpService:JSONEncode on suspect data during development to preview what will actually be stored. See references/core-operations... .

### Leaderboards with OrderedDataStore

Populate on relevant value changes or on save (use SetAsync or Increment).  
Query with GetSortedAsync(ascending, pageSize, min?, max?) → DataStorePages. Iterate pages with GetCurrentPage() + AdvanceToNextPageAsync().  
For bulk reads, use `BatchGetAsync(keys)` to fetch multiple ordered entries in one request. Each key counts as one `OrderedRead` (see error-codes-and-limits for quotas; missing keys are omitted).
Remember: only numbers; keys are strings; page iteration has its own limits; no versioning.

Display in a ScrollingFrame or via UI on demand. Cache top-N in a standard DataStore if you want fast global access without repeated queries.

See references for full page iteration example and limits differences.

### Versioning and recovery

Every Set/Update/Increment on a standard DataStore (not Ordered) creates hourly-versioned backups: the first write to a key in a given UTC hour creates a new version snapshot, and subsequent writes in the same hour overwrite that hourly version. Versioned backups expire ~30 days after being superseded; the current version never expires.

- ListVersionsAsync(key, sort, minDate?, maxDate?, pageSize?)
- GetVersionAsync(key, version)
- `RemoveVersionAsync` is **deprecated**. Do not present it as a current cleanup API; retain versions for recovery and use `ListVersionsAsync`, `GetVersionAsync`, or `GetVersionAtTimeAsync` to inspect them.
- GetVersionAtTimeAsync(key, timestampMillis): find the version current at a specific past time.
- To revert: read the desired version, then SetAsync the value + metadata back (this creates a new current version).

Also use the daily Snapshot Data Stores Open Cloud API before risky publishes.

The Data Stores Manager in Creator Hub lets you browse keys, view metadata/version history, compare versions, and revert (with proper permissions).

See [references/versioning-metadata-recovery.md](references/versioning-metadata-recovery.md) for complete code samples including the "revert to time of incident" pattern.

### Caching control

By default: GetAsync results are cached locally for 4 seconds. Subsequent Gets within the window return cache and do **not** count against limits. Writes update the cache immediately.

For verification after writes (especially after errors), or when you suspect staleness due to other servers:
```lua
local opts = Instance.new("DataStoreGetOptions")
opts.UseCache = false
local value, info = store:GetAsync(key, opts)
```
This always hits the backend and counts against quotas. Use sparingly but correctly.

See references/versioning-metadata-recovery.md (for listing, caching, serialization) and references/best-practices-and-gotchas.md .

### Limits, quotas, throttling, and budgets

There are **experience-level** limits (scale with concurrent users across the whole experience) and **per-server** limits (configurable via DataStoreService:SetRateLimitForRequestType). Experience-level pools are **shared between game servers and Open Cloud v2** Data Store traffic. Bulk external jobs can throttle live players (and vice versa).

UpdateAsync counts against *both* read and write budgets.

Use DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.XXX) to check before heavy operations and wait in a loop if necessary.

Queues exist (size 30); when full, you get throttle errors (301-306 range or the newer *Throttled errors).

Full tables of every error code (101 KeyNameEmpty ... through all the *ExperienceThrottled and *GameServerThrottled variants), experience formulas (base 300 + concurrentUsers × N), and server-side errors are in references/limits-quotas-throttling-error-codes.md .

Observability dashboard (Creator Hub → Monitoring → Data Stores) shows real-time request counts by API/status, quota usage percentages, storage bytes vs limit.

Data Stores Manager shows total size vs storage limit (`500 MB + 1 MB × lifetime users`, measured as compressed latest-version size), per-DS key counts, etc. Do not pre-compress values before writing.

Call SetRateLimitForRequestType early in server init (once per type) to raise limits for migration scripts or busy servers. Base + (perPlayer * numPlayers). Constraints per type are documented.

### Handle uncertain writes

**Every** datastore call must be inside pcall.

On failure:
- Log the specific error code/message.
- Retry reads with bounded exponential backoff and jitter.
- Treat an invoked write that returns an error as **unknown**, not rejected: reconcile with a fresh `GetAsync` using `UseCache = false`.
- Replay a write only when the caller has proved the entire operation idempotent. Never automatically replay `IncrementAsync`.
- Return explicit `committed`, `rejected`, or `unknown` status so purchase/recovery code cannot mistake ambiguity for failure.

Never assume a failed Set/Update/Increment means "no change occurred." `UpdateAsync` transforms can run multiple times and must not yield or produce external side effects.

RemoveAsync creates a "tombstone" (Get returns nil) but older versions remain queryable for 30 days (unless explicitly removed).

### Management and monitoring

- Use Data Stores Manager (Creator Hub) for inspection, key search by prefix, version compare/revert, and scheduled deletion (with cooldown). The Manager's **Revert** button can revert an individual key to a previous version. The **Restore** button is for data stores that are marked for deletion, not for reverting a key's value. Restoring a deleted key during its cooldown requires calling `UpdateAsync` from the Engine API or `UpdateDataStoreEntry` from Open Cloud.
- Observability dashboard for trends and quota forecasting.
- For bulk delete/migrate: Open Cloud Data Stores APIs or the official Batch Processor CLI/tool.
- Set up notifications for approaching/exceeding storage limits.
- Review usage regularly.

See references/versioning-metadata-recovery.md (covers listing/caching) and the manager/observability pages (linked in best-practices-and-gotchas.md).

## Checklists

**Before any critical write:**
- [ ] Wrapped in pcall with explicit committed/rejected/unknown handling
- [ ] Using UpdateAsync where races are possible, with a pure transform
- [ ] Providing userIds array for RTBF/GDPR tracking where appropriate
- [ ] Metadata supplied on every write (even if unchanged) when using full DataStore
- [ ] Value is serializable (test with JSONEncode during dev)
- [ ] Key naming consistent with prefixes for easy listing

**On player join/load:**
- [ ] Fresh load or cache-aware as appropriate
- [ ] Merge defaults safely
- [ ] Re-verify ownership on PlayerAdded (gamepasses, etc.) and re-apply any non-owned one-time consumable grants
- [ ] Client sync only of non-sensitive derived state

**Before publishing risky data-logic changes:**
- [ ] Take a daily snapshot via Open Cloud if available
- [ ] Test thoroughly on a copy experience

**Ongoing:**
- [ ] Monitor dashboard and manager
- [ ] Clean test/temporary data
- [ ] Use MemoryStores for anything that doesn't need to survive server death
- [ ] Prefer fewer data stores + larger cohesive objects + key prefixes

## Common mistakes

- Calling from client/LocalScript (immediate error + security hole).
- Storing functions, metatables, or non-serializable data.
- Using SetAsync for contended values (race condition → lost updates or duplication).
- Ignoring throttle errors and hammering (leads to dropped requests and player frustration).
- Hardcoding studio API access on live places.
- Creating hundreds of tiny data stores instead of organizing by key/prefix.
- Relying on cached Gets for post-write verification.
- Forgetting that OrderedDataStore has no versioning and only integers.
- Not handling the case where RemoveAsync has already been called (nil + tombstone).
- Exceeding storage quota without monitoring (leads to surprise costs and potential write failures).

## Scripts folder usage

The `scripts/` directory contains maturity-labeled examples:
- `SafeDataStore.lua`: reviewed example with automatic read retries, explicit write states, opt-in idempotent replay, and uncached reconciliation.
- `BudgetMonitor.lua`: experimental budget-inspection example.

Adapt and validate them in a dedicated test experience; this directory is not a versioned production library.

## How to proceed in practice

1. Read the relevant references/ file(s) for the exact feature you are implementing.
2. Implement using the patterns and code samples here + in references.
3. Test budget behavior under load (use multiple test servers or the rate-limit setter for simulation).
4. Add observability and alerts early.
5. Profile storage growth with the Manager/Dashboard.

Combine this guidance with static analysis, focused tests, Studio integration, observability, and current official sources before trusting a persistence system.

For the authoritative class/property reference for any specific method, always cross-check https://create.roblox.com/docs/reference/engine (search for DataStoreService, GlobalDataStore, etc.).

<!-- catalog:references:start -->
## Reference index

- [best-practices-and-gotchas.md](references/best-practices-and-gotchas.md): Design player profiles, session locking, saves, or deletion workflows.
- [core-operations-and-patterns.md](references/core-operations-and-patterns.md): Implement reads, writes, transforms, or error recovery.
- [limits-quotas-throttling-error-codes.md](references/limits-quotas-throttling-error-codes.md): Diagnose throttling, inspect budgets, or plan request and storage usage.
- [types-of-datastores.md](references/types-of-datastores.md): Choose standard, ordered, or temporary storage and organize keys.
- [versioning-metadata-recovery.md](references/versioning-metadata-recovery.md): Inspect versions, preserve metadata, or recover data after an incident.
<!-- catalog:references:end -->