Skip to content

Log Code Reference

Every diagnostic Scribe emits carries a stable, machine-readable Code. The string never changes between versions, so you can route, filter and alert on exactly the events you care about without parsing messages. This page lists all of them, grouped by category.

Add a sink and pick what leaves your server. Scribe sends nothing anywhere on its own.

-- ServerScriptService/EmberfallDiagnostics.server.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Scribe = require(ReplicatedStorage.Packages.Scribe)
Scribe.AddLogSink(function(entry)
-- entry = { At, Level, Category, Code, Message, Context }
if entry.Level == "Error" or entry.Level == "Fatal" then
MyBackend:Report(entry) -- everything serious
elseif entry.Code == "PROFILE_SIZE" or entry.Code == "WIPE_GUARD_TRIPPED" then
MyBackend:Report(entry) -- specific codes to watch
end
end)
-- Or pull recent entries on demand.
Scribe.GetRecentLogs({ Code = "PROFILE_LOAD_FAIL", Limit = 50 })
Scribe.GetRecentLogs({ Level = "Error", Limit = 100 }) -- Level filters by minimum severity

Code, Level and Category are typed string unions, so your editor autocompletes them both in a GetRecentLogs filter and on entry inside a sink. Typing Code = "PRO suggests every code beginning with PRO.

Each entry is { At, Level, Category, Code, Message, Context }.

LevelWhat it means for you
DebugDetail for when you are already investigating something.
InfoA normal event worth a record, such as a profile loading.
WarnSomething is off, and the game keeps working.
ErrorSomething failed. Also fires Scribe.OnIssue.
FatalSomething failed and cannot recover. Also fires OnIssue.

Each section heading below is the entry’s Category value, so a row’s section is exactly what a Category filter matches. Context carries structured fields that vary by code: the player, a data path, byte counts, store names and so on.

Repeated failures are folded into one line

These codes are emitted once per problem rather than once per retry: PROFILE_STORE_ERROR, PROFILE_LOAD_FAIL, OFFLINE_READ_FAIL, VERSION_READ_FAIL, LB_ERASE_FAIL, PROFILE_ERASE_FAIL, PROFILE_RESTORE_FAIL, OFFLINE_WRITE_FAIL, MESSAGE_SEND_FAIL, GIFT_DELIVERY_RETRY and PROFILE_OVERWRITTEN. Repeated failures of the same subject inside the retry window collapse into the first line.

Nothing is lost. The first failure of a subject always logs. The line that ends a fold carries Context.Repeats, the number of attempts folded into the previous line. The HealthFailures and DataStoreErrors counters still count every attempt, and FailuresCollapsed counts the suppressed lines. See Diagnostics.

A failure with no subject to fold on logs every time.

Codes that never reach you

Rows marked (Studio only) come from the Scribe Studio’s simulation tools. They cannot fire on a live server, so you can leave them out of production alerting entirely. Rows that say in DevMode are gated the same way in practice, because dev mode defaults to RunService:IsStudio() unless you turn DevMode on yourself.

The console filters separately. The built-in sink forwards entries to Roblox’s structured LogService output keyed by Code, so Creator Analytics aggregates by code rather than by message text, and it forwards only entries at or above the LogLevel option. That defaults to Warn on a live server and Debug in Studio.

Entries below the threshold are still written to the ring buffer and still reach your own sinks, so GetRecentLogs sees them even when the console does not.

CodeLevelMeaning
DATASTORE_CRITICALErrorProfileStore reported the DataStore backend has entered a sustained failure state, so loads and saves are likely failing platform wide.
MESSAGE_SEND_FAILWarnA cross-server message could not be delivered. Usually a DataStore outage. SendMessage returns false for the same condition. Context.ProvablyClean is true only when nothing reached storage; otherwise the write may have committed and lost its answer, so treat the message as retryable rather than lost.
MESSAGE_QUEUE_FULLErrorThe recipient already holds the maximum 1,000 undelivered messages, so the send was refused and SendMessage returned false. Nothing already queued is destroyed. Context.ProvablyClean is true when no attempt in that call reached storage; false means an earlier attempt may already have queued the message, so it can still arrive.
MESSAGE_NO_LISTENERWarnA cross-server message arrived for an active session with nothing connected to Data.OnMessage. It was not acknowledged, so it is handed to the game again on the next load. Usually a startup race.
MESSAGE_HANDLER_ERRORErrorAn OnMessage handler threw. The message was not acknowledged and is redelivered next session, so a handler that throws deterministically stays stuck until it is fixed.
MESSAGE_HANDLER_STALLEDWarnAn OnMessage handler had not returned when the session ended, so the message was not acknowledged and is redelivered next load. Context.Outstanding counts the open deliveries.
MIGRATION_FAILError / WarnA migration step threw, or the migrated result contained unpersistable data caught by a scan before the commit. Migrations run on a clone, so the session is aborted and the unmodified original persists rather than half-migrated data.
MIGRATION_RESERVED_DISCARDEDErrorA migration step changed the reserved _Scribe root and that change was discarded. Only Version was re-stamped. That root is library owned, and it holds the receipt dedupe ring, gift escrow, gift credits, perks, purchase logs and Scribe.Timed deadlines.
MIGRATION_RECONCILE_DEPENDENTWarnOpt-in only, with MigrationShadow = true. A migration step behaves differently against the raw stored data than against the reconciled data. Turn the option on while auditing a migration and off again afterwards, because the shadow re-runs your migration bodies and fires their side effects twice.
LEGACY_IMPORTEDInfoImportLegacyData returned a table and Scribe adopted its top-level keys. The profile then enters the migration chain at version 1 and runs every step. Keys under _Scribe and _ScribeSession are never adopted, and the profile is not reported as new to OnPlayerInit.
LEGACY_IMPORT_FAILErrorImportLegacyData threw, so Scribe refused the load rather than starting the player empty. Loading them empty would commit an empty profile as canonical and strand the legacy data forever.
PROFILE_LOAD_FAILErrorA profile could not be loaded. Under the default LoadFailurePolicy = "Kick" the player is kicked and the code fires once. Under "Wait" nobody is kicked and the load retries.
PROFILE_VERSION_AHEADError / WarnThe stored profile’s version is newer than this server’s code knows about. It fires as Error and kicks under VersionAheadPolicy = "Kick", or as Warn and proceeds under "Allow". Either way it signals a mixed or staged deploy.
PROFILE_SCHEMA_VIOLATIONError / WarnThe opt-in SchemaPolicy check found the stored profile does not match the compiled template: a wrong type, a value outside a declared bound, an over-length string, a value outside an enum, invalid UTF-8, an undeclared key, or a container whose shape or caps changed.
EXPORT_ENCODE_FAILWarnA data export could not JSON-encode a player’s profile, so Export returns nil instead of a payload.
OFFLINE_READ_FAILWarnAn offline read of a non-active user’s profile errored, so GetOffline returns nil. This is a retryable read failure and not a genuinely missing profile.
OFFLINE_WRITE_FAILWarnUpdateOffline could not complete because the DataStore call itself failed, so nothing was stored. A refused write, such as a live session elsewhere or a key that changed under the update, is the compare-and-set working and does not fire this.
NOSAVE_WRITE_REFUSEDWarnUpdateOffline, RestoreVersion, Erase, SendMessage or an offline gift delivery ran on a Mode = "NoSave" bundle, which never writes the store. Nothing was written. Context.Verb names the call.
PROFILE_ERASEDWarnA profile was permanently removed through the erase path. The log records the audit context and whether the leaderboard keys were cleared too.
PROFILE_ERASE_FAILWarnAn erase did not complete, so the profile is still stored and the erase must be retried. A leaderboard key that survived a profile removal reports separately as LB_ERASE_FAIL.
PROFILE_ERASE_RESUMEDInfoAn erase found the marker a previous attempt left and is resuming from its recorded progress.
PROFILE_ERASE_PROGRESS_FAILWarnThe erase could not write its progress and renew its lease, twice in a row; the sweep stops there, and a retry resumes from the last progress it saved.
PROFILE_RESETWarnA profile was wiped back to template defaults because ResetData was enabled. Expected in testing, alarming in production.
PROFILE_RESTOREDWarnA profile was rolled back to an earlier version. The log records the restored version and the audit context.
PROFILE_RESTORE_FAILWarnA restore did not complete, so the stored profile is unchanged. The version was not found, a live session held the key, the live key no longer exists, or the commit failed.
RESTORE_RESERVED_PRESERVEDWarnA restore rolled the profile back, and the library-owned _Scribe root was kept as it was live rather than rolled back with it. That root records events that already happened in the real world, such as settled receipts and spent cooldowns.
SLOW_IMPORTWarnAn ImportLegacyData hook ran for longer than thirty seconds. The hook is unbounded on purpose and holds the session lock throughout, so this separates one patiently waiting on the DataStore budget from one that has hung.
SLOW_LOADWarnA join is still loading after ten seconds. Reported once with the user, elapsed time and current phase, including session acquisition, import or initialization. LoadDuration measures completed joins; ProfileLoadDuration measures store attempts.
PLAYER_LEAVING_HOOK_SLOWWarnOnPlayerLeaving is still running after ten seconds, delaying that player’s final save and session release. The callback is not cancelled. Keep it short; read PlayerLeavingHookDuration for completed callbacks.
PROFILE_SIZEWarnThe profile passed the size-warning threshold and is approaching the 4 MB per-key ceiling. Measured before each save attempt, so it still fires when that save then fails. It stays latched until the size drops back. Also fires OnAnomaly.
PROFILE_STORE_ERRORWarnProfileStore reported a DataStore error against this bundle’s store. Context.Class is Throttled, Failed, Unresolved or Rejected, and Context.Code is the numeric prefix Roblox sent. A nil code means the message did not carry one.
PROFILE_STORE_SIGNAL_MISSINGWarnProfileStore’s error or critical-state signal could not be connected, so save-failure observability for this bundle is off.
PROFILE_TOO_LARGEErrorA save failed while the profile is already over the 3.5 MB warning threshold, so the cause is the 4 MB ceiling rather than a throttle. It will not recover on retry. Every later save, including the leave save, keeps failing until the profile is trimmed.
MODE_OVERRIDES_LEGACYWarnMode was set alongside a legacy persistence flag. Mode wins and the legacy flag is ignored. Remove the flag to silence this.
SAVE_INTERVAL_CLAMPEDWarnThe requested SaveInterval was below the throttling floor and was raised to it.
SAVE_INTERVAL_CONFLICTWarnTwo bundles asked for different autosave cadences. ProfileStore’s autosave period is process wide, so the later value wins. Set it once with Scribe.Configure.
HEALTH_THRESHOLDS_CONFLICTWarnTwo bundles asked for different StatusThresholds. The health machine is a library-level singleton, so the thresholds are process wide and the later value wins — which means one bundle can loosen the Outage gate that refuses Robux for every other bundle. Emitted once per field. Set them once, identically, in every bundle.
SAVE_INTERVAL_FAILWarnApplying the configured SaveInterval threw, so the autosave cadence was not changed.
SAVE_INTERVAL_SETInfoThe configured SaveInterval was applied successfully.
WIPE_GUARD_RATIO_CLAMPEDWarnThe configured WipeGuardShrinkRatio was outside the usable range and was clamped. At or below zero every shrink trips the guard. At or above one it can never trip.
SAVE_WAIT_TIMEOUTWarnA caller waiting on a save gave up. This is unresolved, not failed: the save often lands moments later, so SavesOk may increment for the very save the caller was told false about. Counted as SaveWaitTimeouts.
SIM_LOAD_FAILUREWarn(Studio only.) The Studio plugin armed or triggered a simulated load failure.
SIM_SESSION_STEALWarn(Studio only.) The Studio plugin force-ended an active session to exercise the session-lock path.
STATUS_CHANGEDWarn / InfoThe health machine moved between Healthy, Degraded and Outage. Info when returning to Healthy, Warn when degrading. Also fires OnStatusChanged.
UNKNOWN_ROOT_KEYSWarnIn DevMode, a loaded profile carried top-level keys the template does not declare. That is schema drift a migration should remove.
VERSION_QUERY_FAILWarnEnumerating a profile’s version history failed, so ListVersions returns whatever partial results it gathered.
VERSION_READ_FAILWarnReading a specific historical version errored, so GetVersion returns nil. This is a retryable read failure and not a missing version.
DATASTORE_RECOVEREDInfoThe DataStore backend left its critical state, so the earlier outage has cleared.
MIGRATEDInfoA stored profile was upgraded through one or more migration steps and committed.
PROFILE_LOADEDInfoA profile finished loading and reached Ready, so the data tree is live.
SHUTDOWN_FLUSHInfoShutdown began. New sessions are refused and the save and leaderboard queues are draining.
SHUTDOWN_DONEInfoThe shutdown drain finished. It reports sessions left un-drained when the budget expired, exit hooks skipped for want of budget, and hooks cut off while still running. Ideally all three are zero.
CodeLevelMeaning
PROFILE_OVERWRITTENErrorProfileStore could not recognise the stored value for a key as a profile and replaced it with a fresh template. The session starts from defaults and the previous data is gone. The overwritten profile comes back at load count 1, so it is otherwise indistinguishable from a first-time player.
WIPE_GUARD_TRIPPEDErrorJust before a save, the wipe guard detected an implausible collapse in the data. Also fires OnAnomaly.
ANOMALYWarnAn implausible or suspicious write was detected at a specific path. Often an exploit attempt, sometimes a logic bug. OutOfBounds and OverMaxLength follow BoundsPolicy. Also fires OnAnomaly.
PROFILE_TOO_DEEPWarnThe stored data is nested deeper than the 24-level write limit. Those leaves load and read correctly, and every write to them is refused at runtime. Flatten the shape or migrate it.
TIMED_SWEEP_FAILWarnThe periodic sweep errored while resetting a lapsed Scribe.Timed field, so that field may be left stale.
WIPE_GUARD_BLOCKEDWarnThe wipe guard tripped under WipeGuardPolicy = "Block", so the pending save is held and the last known-good snapshot is persisted instead.
WIPE_GUARD_FORCEDWarnA save the wipe guard had blocked was forced through with Data.Flush(player, { Force = true }), overriding the guard.
WIPE_GUARD_CLEAREDInfoA previously blocked profile passed the check again and resumed saving normally.
ANALYTICS_FAILDebugLogging an economy event threw, so that event never reached the analytics dashboards. One failure is noise. A sustained stream is a broken pipeline.
ECONOMY_FIELD_UNDECLAREDWarnIn DevMode, a tagged Increment or Decrement passed a custom field name the currency did not declare in its Economy config, so it was not recorded. Usually a typo.
ECONOMY_FIELDS_OVERFLOWWarnIn DevMode, a currency declares more than the three custom-field slots Roblox allows, so the fields past the third are dropped.
CodeLevelMeaning
SERIALIZE_FAILErrorEncoding a replication frame threw, so some queued value could not be put on the wire and the affected ops were not sent as-is. The message carries the encode error.
SNAPSHOT_ROOT_DROPPEDErrorA top-level root could not be serialized into a client’s initial snapshot, so that client receives everything except that root. Also fires OnAnomaly.
INIT_APPLIEDDebugThe client finished applying the initial snapshot, so its mirror now matches the server. This is the normal end of the join handshake and needs no action.
INIT_SENTDebugThe server sent a player their initial snapshot. Pair it with INIT_APPLIED on the client to see a join complete end to end.
INIT_SEND_FAILErrorThe initial snapshot for one player was refused by the transport. The player is deliberately not marked ready, so the client’s own retry can still succeed. Counted as InitSendFailed.
CLIENT_HANDSHAKE_TIMEOUTWarnA client never sent its Hello frame within the handshake timeout, so replication never started for them. Most often the bundle was never required on the client. A LocalScript has to require it to begin the handshake.
INIT_UNDELIVEREDWarnThe client did send Hello, and the snapshot Scribe built in reply never reached the transport. The one innocent cause is a transport refusing a payload this size, so the message names MaxOutboundBytes.
MIRROR_RESYNCWarnA frame addressed to one client could not be delivered, so their mirror no longer matches the server and Scribe is re-sending the whole snapshot. This is a repair rather than a loss, and it names a transport that is refusing frames.
OUTBOUND_OVERSIZEWarnA single outbound frame needed more fragments than Scribe considers reasonable, which is 16, or roughly a megabyte at the default MaxOutboundBytes. The frame is still delivered, so this is a cost report. For an Init the message names the largest roots by encoded wire bytes.
OP_BEFORE_INITWarnClient side. A diff frame arrived before the initial snapshot was applied, which is a protocol ordering error. The client re-sends Hello to request a fresh snapshot.
PROTOCOL_MISMATCHWarnA client’s wire version differs from the server’s, so two different Scribe versions are running. Replication is refused for that client.
SCHEMA_MISMATCHWarnSame wire version, divergent templates, so the client derived a different schema hash. Replication is refused for that client.
Two codes for the same symptom, and why

CLIENT_HANDSHAKE_TIMEOUT and INIT_UNDELIVERED both mean a client never got its data, and they are separate codes because the fix is different.

CLIENT_HANDSHAKE_TIMEOUT means the client never asked. Check that a LocalScript requires the Emberfall shared module, or that a custom client transport adapter is present and working.

INIT_UNDELIVERED means the client asked and the answer never left. Check MaxOutboundBytes against what your channel actually accepts, and look at the JoinBytes distribution in Scribe.GetMetrics to see how large the snapshot has grown.

MIRROR_RESYNC matters more than it looks. A loaded client cannot ask for a resync itself, because every Hello is gated on the client not yet being loaded, so this is the only signal that a mirror ever went stale.

CodeLevelMeaning
INBOUND_OVERSIZEWarnAn inbound frame exceeded MaxInboundBytes and was dropped before decoding, so it does not count toward the malformed-frame budget. Logged at most once per second per player, while InboundOversize counts every occurrence.
INBOUND_OVERSIZE_LIMITWarnA player reached the oversize-frame limit of 32 in one session, so the server stops dispatching their inbound frames for the rest of the session. Scribe cannot build a frame over the limit itself, so the one innocent cause is a MaxInboundBytes set below what this game’s own commands produce.
INBOUND_WORK_LIMITWarnA frame would have retained more bytes than MaxInboundRetainedBytes allows, so it was dropped part way through decoding. No reply is sent. Raise the option if your own client legitimately sends payloads this large.
INBOUND_RATE_LIMITEDWarnA player sent raw frames faster than MaxInboundFrameRate, so the frame was dropped before it was read. It drops silently, because replying is the amplification the limit exists to avoid.
MALFORMED_FRAMEWarnA frame could not be decoded or dispatched. On the server the per-player malformed count rises and OnAnomaly fires. Throttled to once per second per player.
MALFORMED_FRAME_LIMITWarnA player reached the malformed-frame limit of 32 in one session, so the server stops dispatching their inbound frames for the rest of the session. A real client never sends malformed frames.
SEND_FAILWarnThe transport adapter threw while sending an outbound buffer. Usually an adapter fault, a player who just disconnected, or a sandboxed Scribe package.
FRAGMENT_REFUSEDDebugClient side. A fragment was discarded for a gap, a replay, or a declared length past the reassembly cap. The logical frame is abandoned and the next one starts clean. Counted as FragmentsRefused.
Why the size limits are three separate ceilings

They catch three different things, and a single number could not.

INBOUND_OVERSIZE is about raw bytes. INBOUND_WORK_LIMIT is about how much memory a frame retains once decoded: 8 KB of one-byte booleans retains a quarter of a megabyte of array storage while 8 KB of one string retains 8 KB, and the depth cap does not see it either, because a flat table of four thousand siblings is only one level deep. INBOUND_RATE_LIMITED is about how often frames arrive, and it is the only ceiling that sees every frame whatever its type, size or validity.

Oversize keeps its own session budget rather than counting toward the malformed one, because the two say different things. A malformed frame was decoded and failed. An oversize frame was never read. A shared counter would let either mask the other.

The work limit is deliberately kept out of the malformed budget as well, because an over-budget frame may be a legitimate payload from your own client and it must not be able to block that player’s session.

The inbound frame rate sits well above CommandRateLimit by default, so a chatty but legitimate client hits COMMAND_RATE_LIMITED, which does reply, long before this. Reaching INBOUND_RATE_LIMITED means traffic no real client produces.

A non-zero FRAGMENT_REFUSED count on a reliable ordered transport is a bug in an adapter, not a network condition. The transport contract forbids the loss and reordering that would explain it.

CodeLevelMeaning
COMMAND_ERRORErrorA handler threw. The caller gets a generic error reply and the traceback stays on the server.
COMMAND_BAD_ARGSWarnAn argument failed the registered Args spec, so the request was rejected with bad-args. Throttled to once per second per player, while CommandsRejected counts every occurrence. Sending more arguments than declared is rejected the same way but is not logged at all.
COMMAND_RATE_LIMITEDWarnA player sent commands faster than CommandRateLimit, so the request was rejected with rate-limited. Throttled to once per second per player. Past a sustained flood the rejection reply is dropped too.
COMMAND_UNKNOWNWarnA client invoked a name the registry does not hold, so it was rejected with unknown-command. Usually version skew, sometimes probing. The throttle clock is per code, so this and COMMAND_BAD_ARGS do not throttle each other.
COMMAND_REPLY_ENCODE_FAILWarnA handler returned a value the wire cannot carry, such as an Instance or a function, so the caller sees reply-encode-failed. The handler’s writes still stand.
COMMAND_REPLY_TRUNCATEDWarnA handler returned more than the eight values a reply frame carries, so the caller saw trailing nils. Pack the extras into a table.
COMMAND_BAD_IDEMPOTENCY_KEYWarnThe command’s idempotency requirement and the request disagreed, or the key was empty, over 64 bytes, or not valid UTF-8. The handler never ran.
COMMAND_IDEM_EVICTEDWarnA player’s 64-record idempotency cache was full, so the oldest settled record was dropped. A retry under that key will run the handler a second time. Also counted as CommandIdemEvicted.
COMMAND_IDEM_SATURATEDWarnAll 64 records were still in flight, so there was nothing safe to evict and the new request was refused with rate-limited. That means 64 handlers are yielding at once for one player.
SIM_COMMANDInfo(Studio only.) A developer invoked a command through the Studio plugin rather than a real client.
Why an idempotency mismatch is refused rather than downgraded

A caller who believes a command is deduplicated and is wrong is worse off than one who knows it is not, so COMMAND_BAD_IDEMPOTENCY_KEY refuses in both directions and never quietly falls back to a plain request. See Commands & Requests.

COMMAND_IDEM_EVICTED exists for the same reason. Reaching it needs far more distinct keys in flight than a real client produces, and when it happens the protection is genuinely gone for those keys, so it is reported rather than dropped silently.

COMMAND_IDEM_SATURATED is the other end of the same cache. Dropping an in-flight record would strand every duplicate parked on it, so nothing is dropped and the new request is refused instead. Look for a handler that never returns, or a client deliberately holding them open.

CodeLevelMeaning
GRANT_FAILErrorA product’s Grant threw, so the grant was aborted fail-closed and the receipt returns NotProcessedYet for Roblox to retry. There is a bug in the Grant function.
GRANT_PARTIALErrorA product’s Grant yielded and then threw. The writes it made before throwing are already in the profile and cannot be taken back, so the receipt is settled as delivered once instead of retried. The purchase log entry is marked Partial. Counted as ReceiptsPartial.
GRANT_SEEDED_ELEMENTWarn(DevMode only.) A Grant brought a container element into existence by writing through a key that named none, so the element was seeded from its defaults. When the key came from stored data, that is a dangling dereference and the player gets goods on something they can never see.
RECEIPT_IN_FLIGHTWarnThe same buyer and purchase id were already being processed on this server, so this delivery was refused and left for Roblox to retry. Seeing it occasionally is the guard working. Seeing it constantly means something is re-invoking ProcessReceipt concurrently.
RECEIPT_UNKNOWN_PRODUCTErrorA receipt arrived for a product id that is not in Products, so it cannot be granted and is deferred. Add the product to the registry.
OWNERSHIP_CHECK_FAILWarnA game-pass ownership call errored, so the check could not be resolved and is treated as not owned. Frequent hits point at a Roblox API problem.
PASS_PURCHASE_UNCONFIRMEDWarnA game-pass purchase reported success but the ownership check did not confirm it, so nothing was credited and no purchase-history entry was written. A genuine purchase the API had not caught up with is credited on the player’s next load. Frequent hits point at API lag; a steady stream for one player is worth looking at.
RECEIPT_OFFLINE_RETRYWarnAn offline receipt could not read the buyer’s saved profile, so the purchase is deferred for Roblox to retry. Repeated hits suggest DataStore read problems.
RECEIPT_RETRYWarnA grant was applied in memory but the save did not confirm within the timeout, so the receipt returns NotProcessedYet. This is safe because the grant is idempotent. Persistent hits point at save latency.
PURCHASE_CLAIM_EVICTEDWarnA profile held MaxPurchaseClaims live Data.Purchase claims, so the one nearest to expiring was dropped. A retry under that key would apply the purchase a second time.
PURCHASE_ID_EVICTEDWarnHistorical code from v2.4.0 and earlier: completed IDs were dropped to meet the count cap. Fixed builds retain them and no longer emit this code. IDs already removed by an older build cannot be reconstructed automatically.
RECEIPT_HISTORY_FULLErrorA new receipt or gift cannot fit its completion record within the history/profile budget. Its grant is deferred without evicting IDs within the 30-day window. Investigate profile size and MaxReceiptHistoryBytes; duplicates still in history can be acknowledged.
UNDECLARED_CATEGORYWarnIn DevMode, a purchase-log entry used a category string that is not in the declared set. Usually a typo.
UNDECLARED_PERKWarnIn DevMode, a perk key was granted or referenced that is not in the Perks registry. Usually a typo.
UNKNOWN_OWNS_KEYWarnIn DevMode, Owns or OwnsAsync was called with a key that is not a registered pass, a declared perk, a product grant, or RobloxPlus, so it will always return false. Warned once per key.
POLICY_READ_FAILWarnThe player’s policy could not be read after three attempts, so a PaidRandom entry stays policy-pending for them and every prompt for it refuses. Logged once per player; the next check re-arms one read at most every 30 seconds.
PAID_RANDOM_RECEIPTWarnA receipt for a PaidRandom product arrived from a player whose policy restricts paid random items. It was granted, because the money has already moved; only a prompt made outside Data.PromptPurchase can produce it, so the fix is that call site.
PRODUCT_INFO_FAILWarn(Client.) Marketplace refused every attempt to read a pass or product’s info, so its price stays nil rather than falling back to the undiscounted catalog price. Usually rate limiting. The read is retried by the next GetProductInfo call.
PRODUCT_INFO_THROTTLEDWarn(Client.) A product info read failed and will be retried with backoff. Roblox rate-limits GetProductInfoAsync and does not publish the limit.
SIM_RECEIPTWarn(Studio only.) The Studio plugin injected a mock receipt. Injection requires the resolved Mode = "Mock", so no real store is ever touched.
PERK_GRANTEDInfoA perk flag was set on a ready player.
PERK_REVOKEDInfoA perk flag was cleared on a ready player.
PURCHASE_DUPLICATEInfoA Data.Purchase carried an idempotency key that had already been applied, so the grant was skipped and the call returned success. Keys expire at PurchaseClaimTTL.
RECEIPT_DUPLICATEInfoA receipt whose purchase id was already processed was re-acknowledged, returning PurchaseGranted without granting again. Normal Roblox retry behaviour.
RECEIPT_GRANTEDInfoA purchase was applied and durably saved. This is the expected success path for CoinPack500 and GemPack100.
RECEIPT_HANDLER_BOUNDInfoScribe bound MarketplaceService.ProcessReceipt and now handles developer-product receipts. Set OwnReceipts = false if your game runs its own handler.
Two receipt codes worth understanding

GRANT_PARTIAL. A yielding Grant cannot run inside a transaction, so it runs without rollback. When it throws half way, its writes are already in the profile. Answering NotProcessedYet would make Roblox retry and re-apply those writes on every attempt, which in testing produced 1000, then 2000, then 3000 coins, all persisted. Settling the receipt as delivered once is the smaller loss, and the code is raised so an operator can compensate the player by hand.

The fix is always the same. Move the async work out of Grant, either before the prompt or afterwards through OnSave, and the grant becomes atomic again. See Monetization.

RECEIPT_IN_FLIGHT. The persisted dedupe ring is a completion record, written only after the grant commits. Between the duplicate check and that write, a second delivery of the same receipt would otherwise see “not yet granted” and grant again. Every yield in that window widens it, including a yielding Grant, an offline round trip, or a gift’s cross-server message.

The guard never waits. It refuses immediately and leaves the retry to Roblox. By the time Roblox retries, the first call has either marked the receipt processed, so the retry answers PurchaseGranted, or failed, so the retry is clean.

CodeLevelMeaning
GIFT_CREDIT_REFUND_FAILErrorA gift delivery failed AFTER the buyer’s paid credit had been spent, and the buyer had already left, so the refund had to go to their stored profile — and that write failed too. The credit is lost and needs restoring by hand; the code names the user and the product.
GIFT_INTENT_WRITE_FAILErrorThe durable save of a pending gift intent failed right before prompting, so the prompt is refused and no Robux is charged.
GIFT_UNKNOWN_PRODUCTErrorA paid gift names a product absent from both Products and RetiredProducts. This message remains queued, while other messages can still deliver. Restore the historical name/ID/grant handler in RetiredProducts; a large backlog can fill the shared inbox.
GIFT_AIM_CAP_REACHEDWarnThe buyer’s durable gift-aim store is full. New paid gifts are refused. An intent that cannot be archived stays in the pending store so its destination is preserved.
GIFT_AIM_EXPIREDWarnHistorical: earlier versions dropped an old gift aim to make room, potentially losing an unobserved receipt’s destination. Version 2.5.0 preserves unresolved aims and no longer emits this code.
GIFT_AIM_SETTLEDWarnA receipt arrived after its gift intent had been swept as stale and was settled from the durable aim, so it reached the recipient it was paid for rather than the buyer.
GIFT_CREDIT_ISSUEDWarnA giftable perk was bought with no gift intent while the buyer already owns it, under NoGiftIntentPolicy = "GrantOrCredit", so a re-aimable credit was written instead of a no-op grant. Also fires OnGiftCredit.
GIFT_CREDIT_UNCONFIRMEDWarnA gift bought with a paid credit could neither be confirmed delivered nor proved undelivered, so the credit was deliberately NOT refunded: the gift may already be queued, and handing the credit back would let one payment grant twice under a fresh id. It names the buyer, the product and the recipient so the rare genuine loss can be compensated by hand.
GIFT_DELIVERY_RETRYWarnCross-server gift delivery failed, so the receipt returns NotProcessedYet and Roblox will retry. Repeated hits point at a DataStore messaging problem.
GIFT_INTENT_EXPIREDWarnA stored intent names a different product using the same Roblox product ID. It cannot route this product’s receipt and is ignored or cleared. Restore the historical product identity rather than reusing IDs; age alone no longer invalidates an unresolved intent.
GIFT_INVALID_RECIPIENTErrorA stored gift intent or aim has a non-positive, fractional, non-finite, or otherwise invalid recipient ID. The receipt stays pending for repair; Scribe does not save a new receipt destination or deliver to that invalid ID.
GIFT_NO_INTENTWarnA giftable perk was bought with no matching intent and the buyer does not already own it, so the perk was granted to the buyer. An expected fallback.
GIFT_RECIPIENT_ALREADY_OWNSWarnBetween prompt and receipt the recipient acquired the perk anyway, so the purchase became a re-aimable credit for the buyer rather than a wasted grant. Also fires OnGiftCredit.
RECEIPT_DECLINED_PENDING_CREDITWarnA no-intent perk purchase arrived while the buyer already owns the perk and still holds an unused credit. The receipt stays pending; spending the credit allows a later retry to proceed. This does not issue a refund.
RECEIPT_HELDWarnA no-intent perk purchase arrived while the buyer already owns the perk under NoGiftIntentPolicy = "Hold", so the receipt is held for retry instead of auto-crediting.
GIFT_CREDIT_UNKNOWN_PRODUCTWarnA player holds credits for a name absent from both product registries. Restore its historical handler in RetiredProducts to allow redemption. Paid credits are never deleted automatically.
GIFT_CREDIT_USEDInfoA buyer redeemed an existing credit to deliver a gift, so no new purchase was charged.
GIFT_RECEIPT_GRANTEDInfoA gift purchase was durably delivered to the intended recipient and logged as sent.
CodeLevelMeaning
LB_ERASE_FAILErrorA board removal failed during user erasure, so the player’s score may still sit on at least one board and the erase must be retried. One entry per erase, with Context.Boards listing every board that failed.
LB_INTERVAL_CLAMPEDWarnA board’s requested interval was below its minimum and was raised to it. RefreshInterval has a 60 second minimum for global boards and 1 second for server boards. WriteInterval has a 1 second minimum.
LB_QUEUE_OVERFLOWWarnThe write queue hit its cap, so the oldest pending score write was dropped. Score updates are arriving faster than the pacer can persist them.
LB_READ_FAILWarnA board refresh failed, so its rankings could not be updated this cycle. Store failures are throttled to one line per code every 30 seconds, and the line carries how many it suppressed; the counters keep counting every attempt. Studio with API access off is reported once for the whole session instead, because it is a setting rather than an outage.
LB_BUDGET_DEFERREDWarnAutomatic shared pacing or low DataStore allowance postponed a background request. The write remains queued or the refresh remains due, so boards update more slowly. This can occur without BudgetPolicy = "Defer". Logged at most once per 30 seconds.
LB_WRITE_DROPPEDWarnRoblox permanently rejected this score write. Automatic retries pause until its value changes; inspect the error/configuration. Transient failures keep retrying with backoff. Throttled like LB_READ_FAIL.
LB_SCORE_OUT_OF_RANGEWarnThe score fell outside what an ordered key can hold, so the write was dropped rather than queued, since it would be rejected on every attempt. Lower Scale, or bound the stat.
LB_WRITE_FAILWarnOne score write failed. Transient failures retain the latest value and retry with capped backoff through the shared request budget. Throttled like LB_READ_FAIL; the log interval is not the request rate.
LB_STAT_RESOLVE_FAILWarnA board’s Stat path failed to resolve for one player at load, so that board is not tracking them this session. The other boards are unaffected.
LB_UNKNOWN_BOARDWarnIn DevMode, a board name was requested that is not declared in the Leaderboards option. Board names are case sensitive, so "toplevel" does not match TopLevel. Warned once per name.
LB_SHUTDOWN_FLUSHInfoThe write queue was drained during shutdown, reporting how many writes landed and how many remained.
SIM_LB_FLUSHInfo(Studio only.) A developer drained the write queue from the Studio plugin.
Why board failures never move the health status

Leaderboard read and write failures deliberately do not feed the global health machine. Routine ordered-store throttling would otherwise flip the status to Outage, and Outage blocks Robux receipts. A slow TopLevel board must never stop players buying GemPack100.

LB_SCORE_OUT_OF_RANGE fires for two different reasons and the message says which. On a plain numeric stat, the score multiplied by the board’s Scale passed the exact-integer range an ordered key can hold without losing its low digits. On a Scribe.Big stat, the score was negative, because the packing has no sign bit, or its exponent passed the board’s cap for its configured significant figures. Scale does not apply to a big board.

Sustained LB_BUDGET_DEFERRED means scheduled board traffic exceeds the available allowance. Check the request type in the log: raise WriteInterval to reduce score writes, or RefreshInterval to reduce sorted reads. Fewer global boards reduce both; Scope = "Server" avoids DataStore requests entirely.

Every one of these is about an exchange in progress. None of them means value was lost: the design’s whole claim is that an exchange conserves whatever happens to it, and these say WHERE it currently is. See Exchange.

CodeLevelMeaning
EXCHANGE_INIT_TAMPERErrorOnPlayerInit or a Scribe.Dynamic factory changed the in-flight exchange ledger. Those hooks receive the profile directly, before the tree exists, so the usual refusal has nothing to refuse through. The change is reverted; find the line that writes into _Scribe.
EXCHANGE_PARKEDWarnA delivery could not be applied, usually because the destination is full or the key is taken. The item is waiting in the player’s inbox and is retried on every load and on the sweep. Nothing is lost.
EXCHANGE_REGISTRATION_REFUSEDErrorA Exchangeable declaration names a path that cannot be moved safely. Raised at startup, by name; see Exchange.
EXCHANGE_RESET_REFUSEDErrorResetData was asked to wipe a profile holding an in-flight or undelivered exchange. Refused, because the wipe would take the escrow with it and leave nothing to return. Resolve or discard the exchange first.
EXCHANGE_RESOLVEDInfoAn interrupted exchange reached its terminal state on load or on the sweep.
EXCHANGE_RESOLVE_ERRORErrorResolving one exchange raised. Other exchanges on the same profile are unaffected; this one keeps its value and is retried.
EXCHANGE_STORE_UNAVAILABLEErrorThe verdict key could not be reached, so no exchange can be started or resolved on this server. Scribe deliberately does NOT fall back to a local store: a verdict nothing else can see is worse than no verdict.
EXCHANGE_UNRESOLVEDWarnAn exchange could not be resolved this time, usually because no verdict could be established. The value stays where it is and the next load or sweep tries again.
CodeLevelMeaning
DERIVED_ERRORErrorA derived field’s compute function threw. The field keeps its previous value and the write that triggered the recompute still succeeds, so the symptom is a field that stops tracking its inputs. Logged once per field per session.
DERIVED_FEEDBACKErrorA Changed listener kept writing an input from inside the recompute its own field triggered, so the settle pass gave up after four rounds. Move the write out of the listener, or derive the value instead of writing it.
DERIVED_MISMATCHWarn(DevMode only.) The server computed a different value than this realm did for a field both compute locally, so the compute function is not pure. Look for os.time, math.random, or an upvalue that differs between the two realms. The server’s value is applied.

A derived compute runs on both realms, which is what makes Emberfall’s Level free to replicate: only Xp crosses the wire. Outside DevMode nothing is sent, so nothing is compared, which is why an impure compute has to be caught in Studio before it ships. See Derived Fields.

CodeLevelMeaning
LISTENER_ERRORErrorA listener or callback you supplied threw while processing a data change. Scribe caught it and reported it.
ON_PLAYER_INIT_ERRORErrorThe OnPlayerInit callback threw while initializing a newly loaded profile.
DYNAMIC_DEFAULT_FAILEDErrorA Scribe.Dynamic factory threw while seeding a field on a brand-new profile, so that field keeps its declaration-time baseline.
PROFILE_UNPERSISTABLEErrorA value in the loaded profile cannot be stored and would fail the whole save: invalid UTF-8, an unserializable value, a table mixing array indices with string keys, or a table nested past 64 levels. Scribe logs the exact path and leaves the value in place for you to fix. Also fires OnAnomaly.
SUBSYSTEM_HOOK_ERRORErrorAn internal load hook for Timed, Monetization, Leaderboards or Replication errored when a player’s entry became ready.
API_NAME_COLLISIONWarn / ErrorA template field name collides with a reserved accessor or method name, so that field is unreachable through the typed API. Rename it.
DEV_WARNINGWarnIn DevMode, a call pattern that is almost certainly a bug was detected. The current case is Decrement with a negative delta, which adds instead of subtracting. The message names the path.
SANDBOXEDWarnThe Scribe package has Roblox’s Sandboxed property set, so it cannot fire its replication events and nothing will sync. Set Sandboxed = false on the Scribe package and on ProfileStore. Usually caused by inserting Scribe from the toolbox instead of syncing from source.
UNKNOWN_OPTIONWarnIn Studio, an option key passed to Scribe was not recognised and would be silently ignored. Boot the game in Studio at least once after changing options, or the typo goes unreported.
DEBUG_HOOK_DUPLICATEWarn(Studio only.) A second debug hook tried to attach, so only the first bundle is inspectable in the plugin.
DEBUG_HOOK_ERRORWarn(Studio only.) The debug bridge dropped a diagnostic chunk. Only the inspector view is affected.
DEBUG_HOOK_FAILWarn(Studio only.) Attaching the debug hook errored, so the game runs normally and is not inspectable this session.
DEBUG_HOOK_WRITESWarn(Studio only.) The plugin toggled its ability to write to live data.
SIM_STATUSWarn(Studio only.) The plugin forced the health status for testing, which broadcasts the new status. Also fires OnStatusChanged.
DEBUG_HOOK_ATTACHEDInfo(Studio only.) The Studio plugin finished its handshake and attached.
DEBUG_HOOK_ATTRIBUTIONInfo(Studio only.) The plugin turned write-source attribution on or off.
SERVER_STARTEDInfoThe Scribe server finished initializing, reporting the field count and the transport name.
Two lifecycle checks worth knowing

The client reports a shadowed root field at Error, on live servers as well as in Studio, because that is the case where Data.Coins silently turns from an accessor into a method.

The server’s checks stay at Warn and are gated on DevMode. Those are a root name matching a Data API name, and any field named like an accessor method such as Get. DevMode defaults to Studio but is an ordinary option, so a headless or CI run can turn it on.

Both realms scan after the whole Data surface is built, so a name assigned late in construction is judged like any other. See Configuration.

PROFILE_UNPERSISTABLE runs on every load. The scan covers the whole profile, not only what load-time code wrote, and there is no option to turn it off.

The usual source is a Scribe.Dynamic factory or the OnPlayerInit hook, both of which bypass the accessor write guard. Corruption that came straight out of the DataStore is reported the same way. The entry is non-fatal and names the exact path, because the alternative is a save that fails silently forever.

  • Diagnostics shows the counters and health machine these codes accompany.
  • Scribe Studio renders the log ring as a filterable live panel.
  • Configuration covers LogLevel, LogRingSize and DevMode, which decide which of these you actually see.
  • Commands & Requests explains the COMMAND_* codes from the caller’s side.
  • Monetization explains the receipt and gift codes in the order they actually happen.