Skip to main content

Comparing libraries

This page sets Geode next to four other Roblox DataStore libraries: ProfileStore, ProfileService, Lapis and DataStore2. It lists what each one does and how, taken from their source code at the versions below. It doesn't rank them. Measured numbers are on Benchmarks, and moving data across is covered in Coming from ProfileStore, ProfileService or Lapis.

Versions read for this page:

LibraryRepositoryVersion read
Geodeqvyra/geode0.6.1
ProfileStoreMadStudioRoblox/ProfileStorecommit 45c9847 (2025-07-31), the repository has no releases
ProfileServiceMadStudioRoblox/ProfileServicecommit d736800 (2024-10-13), the repository has no releases
Lapisnezuo/lapisv0.3.3 (commit f140aca), with its dependency evaera/promise 4.0.0
DataStore2Kampfkarren/Robloxds2-1.4.0 (commit ab81664)

At a glance​

FeatureGeodeProfileStoreProfileServiceLapisDataStore2
Session lockingYes, a lease in the recordYes, a session in the recordYes, a session in the recordYes, a lock id in the recordNo
Taking a held lockAsks the holder, takes it after 15 s by default (stealPolicy)Asks the holder, takes it after 40 s, or at once with Steal = true"ForceLoad" asks the holder and takes it after 8 tries, "Steal" takes it nowRetries, then fails. No way to take itNot applicable
A lock nobody renewsFree after leaseTimeout, 300 s by defaultTreated as dead after 630 s without an updateTreated as dead after 30 minutes without an updateFree 30 minutes after the key's last updateNot applicable
Server hopsMessagingService request plus a MemoryStore key, the old server releasesMessagingService request, the old server saves and ends its sessionThe old server sees the request on its next save. ListenToHopReady for teleportsThe new server keeps retrying until the old one closesNo lock to wait for
Schema and validationTyped schema checked on every write, plus guardsNone, a template filled in with ReconcileNone, a template filled in with ReconcileA validate function, checked on load and saveChecks that saved values are storable
MigrationsKeyed by schema version, newer data refusedNoneNoneA list of migrations, can be marked backwards compatibleNone
Codecs for Roblox typesVector3, Vector2, CFrame, Color3, UDim2, DateTime, EnumItem, BrickColor, customNone, the docs say to serialize firstNone, the docs say to serialize firstNoneBeforeInitialGet and BeforeSave hooks to write your own
Developer productsBuilt in, purchase ids kept per profileA documented pattern, not built inA documented example keeping purchase ids in MetaTagsA documented example keeping purchase ids in the dataNot covered
Messages to a profilestore:mail, applied once on the next load, in stores made with mailbox = trueMessageAsync with MessageHandler, also reaches a loaded profileGlobalUpdates with active and locked stagesNoneNone
Versions and rollbackstore:history, store:version, store:rollback, optional audit logVersionQuery and GetAsync by version, a viewed profile can be written backProfileVersionQuery, ViewProfileAsync, OverwriteAsyncNo version APIThe default OrderedBackups method keeps every save under its own key
Replication to clientsBuilt in, per pathNoneNoneNoneNone, OnUpdate callbacks on the server
Testing without RobloxLune tests on in-memory mocks with fault injectionProfileStore.Mock inside a Roblox sessionProfileStore.Mock inside a Roblox sessionTakes any DataStoreService, its own tests run inside RobloxIts own tests use a mock DataStoreService inside Roblox
TypingLuau strict, TypeScript typings in the repositoryLuau types in the moduleUntyped LuaLuau types in the moduleUntyped Lua
LicenseMITApache-2.0Apache-2.0MITIts own license, see below
Maintenance status0.6.1 released 2026-10-31Last commit 2025-07-31The README says it's no longer supported and points to ProfileStorev0.3.3 from 2024-08-24, last commit 2025-02-12ds2-1.4.0 from 2021-05-23, last commit 2023-03-17

Details​

Geode​

A profile is one DataStore key holding the data and its bookkeeping. The lease, the data and the purchase ids are written together by one UpdateAsync, so a server whose lease was taken can't write anymore. Loads that find a live holder ask it to release over MessagingService, and a short lived MemoryStore key repeats the answer in case a message is lost. Neither is needed for correctness. See How it works for the full path.

ProfileStore​

The session is MetaData.ActiveSession = {PlaceId, JobId, session id} inside the saved record. A server that finds another session writes ForceLoadSession, sends EndSession to the holder on the MessagingService topic PS_<session id>, reads again after FIRST_LOAD_REPEAT (5 s) and then every LOAD_REPEAT_PERIOD (10 s), and takes the session once SESSION_STEAL (40 s) has passed. The holder saves and ends its session when the message arrives, and also when one of its saves sees a pending ForceLoadSession. Autosave runs every AUTO_SAVE_PERIOD (300 s). StartSessionAsync gives up after START_SESSION_TIMEOUT (120 s) of failed requests. In Studio it first writes to a DataStore named ____PS to find out whether API access is on, and runs on an in-memory store when it isn't. It's on Wally as lm-loleris/profilestore, latest 1.0.3 when this page was written.

ProfileService​

The session is MetaData.ActiveSession = {PlaceId, JobId}. LoadProfileAsync(key, handler) calls the handler when the profile is held, and the handler answers "Repeat", "Cancel", "ForceLoad" or "Steal". With "ForceLoad" the loading server writes ForceLoadSession and the holder releases when one of its autosaves (spread over AutoSaveProfiles, 30 s) sees it. After ForceLoadMaxSteps (8) tries the loading server takes the session. Every DataStore call for one key waits RobloxWriteCooldown (7 s) after the previous one, so the 8 tries take roughly a minute when the holder never answers. TODO(verify) that time, the bench didn't measure a holder that never answers. In the benchmarks a holder that kept the profile let go after about 21 s, on its next autosave. It doesn't use MessagingService. The README asks new projects to use ProfileStore.

Lapis​

The lock is a lockId stored with the data. A load that finds a lock younger than LOCK_EXPIRE (30 minutes, measured from the key's last update) retries loadAttempts (20) times, loadRetryDelay (1 s) apart, then fails with "Could not acquire lock". There's no option to take a held lock. Every request waits until the request budget for its type is above zero, so Lapis queues instead of going over the budget. Pending saves and closes for one key are combined into one request. Data is deep frozen by default and replaced with document:write. Its README warns that it hasn't been used in a large production game yet.

DataStore2​

There's no session lock, so two servers can hold and save the same player's data at once. Data is cached per player and saved when the player leaves, on game:BindToClose and on :Save(). The default saving method, OrderedBackups, writes every save to a new key and keeps an OrderedDataStore pointing at the latest one, so old saves stay readable. The Standard method uses one key per player. In Studio it doesn't save unless a SaveInStudio BoolValue in ServerStorage is set. Combine stores several named values under one key.

Its license allows copying and changing the code. When a published game uses a changed copy, the changes have to be published too, for example as a fork on GitHub. Read the license file for the exact terms.