Skip to main content

How to design Roblox daily rewards without duplicate grants

Diagram of server-authoritative Roblox LiveOps systems and their protected runtime boundaries.

A practical server-authoritative design for Roblox daily rewards that resists repeated requests, concurrent servers, datastore failures, and partial grants.

Treat a claim as a transaction, not a button press

A daily-reward interface looks simple: show a calendar, highlight today, and let the player claim. The difficult part is everything behind that button. A client can send the same request repeatedly. Two servers can observe the same stored state. A datastore request can fail after part of the reward logic has run. A player can disconnect between persistence and presentation. If the implementation treats each request as permission to grant, duplicate currency and items are an expected outcome rather than an edge case.

The safer model is a server-owned transaction. The client asks to claim, but it never decides whether a reward is due, what the reward contains, or whether the grant succeeded. The server derives the current reward period, loads the authoritative profile state, validates the configured reward, attempts one atomic state transition, and only then returns a result the client can display.

That separation is the central rule: the user interface communicates intent; the server owns eligibility and mutation.

Define the smallest authoritative state

Daily rewards do not need a history of every interface interaction. They need enough durable state to answer three questions: which reward period was last committed, what streak position follows from that state, and whether the current request can move the state forward.

A practical profile usually needs a stable period key, a last-successful-claim key, and a streak value. The period key should come from server time and a documented reset policy. Whether the product resets at UTC midnight, at a configured regional boundary, or after a rolling duration is a product decision, but the decision must be deterministic on the server. Client clocks should never determine eligibility.

Keep configuration separate from player state. The configured calendar can define reward identifiers, quantities, currency names, streak behavior, and active dates. Player state should refer to a validated configuration version or stable reward identifier rather than copying arbitrary client payloads into storage.

Validate the remote before touching storage

Roblox security guidance treats every value crossing the client-server boundary as untrusted. For a daily claim, the request should be deliberately boring. In many designs the server needs no reward amount, streak number, or day index from the client at all; it can derive those values itself.

Before datastore work begins, the server should apply a small rejection chain:

  • confirm the request arrived on the intended server RemoteEvent or RemoteFunction;
  • reject unexpected types, extra identifiers, or malformed payloads;
  • rate-limit claim attempts per player on the server;
  • verify the player profile and relevant feature configuration are available;
  • refuse requests while a claim for that player is already being processed locally.

Rate limiting is not the duplicate-grant guarantee. It protects server capacity and reduces abuse. The durable guarantee must still survive two servers, retries, reconnects, and process restarts.

Make eligibility and commitment one atomic update

A `GetAsync()` followed later by `SetAsync()` leaves a race window. Two servers can read the same unclaimed state, both decide the player is eligible, and both write a grant. Roblox documents `UpdateAsync()` as the safer multi-server operation because it reads the current value and retries the callback when concurrent updates conflict.

Put the eligibility decision inside that atomic update. The callback receives the latest stored value, checks whether the current period has already been committed, and returns either a new state or `nil` to cancel. The callback must not yield, so it should not call external services, wait for instances, or perform the visible reward grant itself.

The committed state should include a durable grant marker: the reward period and the stable reward identifier or transaction key that won the update. If the callback discovers that marker already exists, the correct result is “already claimed,” not another grant.

This pattern converts repeated requests into repeated reads of the same outcome. The first valid transition wins. Later attempts observe the committed marker and become no-ops.

Close the gap between persistence and reward delivery

Atomic profile state prevents two claim transitions, but the design still needs a clear delivery order. If currency is added before the claim marker is durable, a datastore failure can leave value granted without a record. If the marker is stored first and the server crashes before delivery, the player can have a committed claim without the item.

The robust answer is an idempotent grant record. Commit a transaction identifier and a delivery state, then make reward delivery safe to repeat against that identifier. Currency, inventory, badge, or entitlement services should recognize that a particular transaction has already been applied. A recovery path can inspect committed-but-undelivered records and retry them without creating a second reward.

Not every experience needs a complex ledger, but every experience needs an explicit answer to the partial-failure question. “The request probably finishes” is not a guarantee.

Preserve operation order during retries

Datastore calls can fail, and the LiveOps Goblin runtime foundation uses a safe datastore wrapper with retry and backoff behavior. Retrying is not the same as sending the same operation again from several independent tasks. Roblox warns that naïve retries can reorder stateful operations.

Use one serialized claim path per player, preserve the order of profile changes, and keep the transaction identifier stable across retries. Retry only the failed operation with bounded backoff. Do not recompute a new reward period or transaction key on each attempt. If the player leaves, durable recovery should be able to continue from stored state rather than from an in-memory flag.

Diagnostics should distinguish rejected requests, rate limits, datastore warnings, already-claimed outcomes, committed claims, and delivery recovery. Those events are operational evidence; they should not expose secrets or fabricate activity when no analytics provider is configured.

Keep configuration safe before it reaches the game

Duplicate protection is weakened when the exported reward calendar itself is ambiguous. LiveOps Goblin validates impossible quantities, missing datastore keys, ambiguous currency names, duplicate codes, and expired schedules before producing a versioned Roblox configuration.

A reward definition should use stable identifiers, bounded quantities, known currency or inventory targets, explicit schedule rules, and a version that can be traced in diagnostics. Empty calendars should remain empty until a creator adds real rewards. Demo values in production configuration are more dangerous than an honest empty state because they can become real economy mutations.

When a calendar changes, decide what happens to players mid-streak. Stable reward identifiers and versioned configuration make that policy testable. Position-only assumptions make it easy for “day three” to refer to different rewards before and after an update.

Test the failures, not only the happy path

A useful daily-reward test matrix should include more than “claim once and receive coins.” At minimum, exercise:

  • two rapid requests from the same client;
  • requests from two server contexts against the same stored profile;
  • a claim for a period already committed;
  • malformed remote payloads and rate-limit behavior;
  • datastore conflict, transient failure, and exhausted retry paths;
  • disconnect after commitment but before client confirmation;
  • delivery retry using the same transaction identifier;
  • a calendar version change during an active streak;
  • a missing or invalid reward configuration;
  • analytics disabled with diagnostics still functioning.

The expected result is not merely “no crash.” Each test should assert the final durable state, the number of grants applied, the client-visible outcome, and the diagnostic event.

A concise implementation checklist

The final design can be reviewed with ten questions. Is eligibility derived on the server? Is reset time deterministic? Does the client send only intent? Are remote requests validated and server-rate-limited? Is the claim transition atomic? Does the stored state include a stable transaction marker? Can reward delivery be repeated without duplication? Do retries preserve order? Are configurations versioned and validated? Do concurrency and partial-failure tests prove exactly one durable grant?

When all ten answers are concrete, a daily reward stops being a fragile button handler and becomes a small, understandable LiveOps transaction.

Related systemExplore LiveOps Goblin