How do I make a daily reward in Roblox?
A daily reward is a timestamp, not a timer. Save os.time() the moment the player claims, and on every join compare it with now: if more than 24 hours passed, the button is live again. The streak is a second number you reset when the gap passes 48 hours.
In short
- Store the claim time, never a countdown. A countdown dies with the server.
- 24 hours since the last claim unlocks the button; 48 hours breaks the streak.
- Decide the reward on the server. The client only asks.
A daily reward asks one question: how long since this player last claimed? Everything else — the streak, the escalating prize, the glowing button — is drawn from that one number. Players close the game, the server shuts down, a week goes by: a saved timestamp survives all of it and a running timer survives none of it.
Steps
1. One DataStore entry per player
Keep the claim time and the streak together in a table. One UpdateAsync writes both, so a player cannot end up with a new claim time and an old streak.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("DailyReward_v1")
local DAY = 24 * 60 * 60
local function load(userId: number)
local ok, data = pcall(function()
return store:GetAsync("u_" .. userId)
end)
if not ok or type(data) ~= "table" then
return { lastClaim = 0, streak = 0 }
end
return data
end
2. Ask whether the button is live
The answer is a subtraction. Write it once, on the server, and let both the button and the claim read it — two copies of this rule is how a player gets a button that does nothing.
local function status(data)
local since = os.time() - (data.lastClaim or 0)
return {
canClaim = since >= DAY,
-- A gap over two days means they missed a whole day: the streak is gone.
streakAlive = since < 2 * DAY,
secondsLeft = math.max(0, DAY - since),
}
end
3. Claim on the server, never on the client
The client presses a button; the server decides what that is worth. A reward worked out on the client is a reward anybody can rewrite.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local claimEvent = Instance.new("RemoteEvent")
claimEvent.Name = "ClaimDaily"
claimEvent.Parent = ReplicatedStorage
local REWARDS = { 50, 75, 100, 150, 250, 400, 1000 }
claimEvent.OnServerEvent:Connect(function(player: Player)
local data = load(player.UserId)
local s = status(data)
if not s.canClaim then return end
local streak = s.streakAlive and (data.streak or 0) + 1 or 1
local day = math.min(streak, #REWARDS)
local amount = REWARDS[day]
local ok = pcall(function()
store:UpdateAsync("u_" .. player.UserId, function()
return { lastClaim = os.time(), streak = streak }
end)
end)
-- Nothing is paid out until the write succeeded: a reward given on a failed
-- save is a reward the player gets again on the next join.
if not ok then return end
local stats = player:FindFirstChild("leaderstats")
local coins = stats and stats:FindFirstChild("Coins")
if coins then
coins.Value = coins.Value + amount
end
claimEvent:FireClient(player, { claimed = true, day = day, amount = amount })
end)
4. Tell the client what to draw
Send the status on join and let the GUI render it. The client never computes the unlock; it is told.
Players.PlayerAdded:Connect(function(player: Player)
local data = load(player.UserId)
task.defer(function()
claimEvent:FireClient(player, status(data))
end)
end)
Which clock to compare against
| What you compare | Survives a restart | Survives a timezone | Use it when |
|---|---|---|---|
os.time() difference | yes | yes | Almost always: "24 hours since you claimed" |
Calendar day (os.date("*t")) | yes | no — the server's UTC day, not the player's | You want "one claim per calendar day" and accept UTC |
tick() / os.clock() | no | n/a | Never for this: both reset when the server does |
A running while loop | no | n/a | Never. The player closes the game and your timer dies |
FAQ
Why does my reward reset every time the server restarts?
You are measuring with something that restarts too. tick(), os.clock() and a while true do task.wait(1) end counter all start from zero with the server. os.time() is the real clock, in seconds since 1970, and it is the same number on every server in your experience.
How do I stop someone claiming twice by rejoining?
The check is on the server and the stored timestamp is the only authority. Rejoining re-reads the same lastClaim, so since is still under a day and the claim returns early. Never keep canClaim in a client-side variable — that one does reset on rejoin.
Should I use UpdateAsync or SetAsync?
UpdateAsync here. Two servers can hold the same player for a moment during a teleport, and SetAsync lets the second one overwrite the first. UpdateAsync reads and writes as one operation, which is what keeps the streak from going backwards.
Where do I put the streak on screen?
Next to the button, as the number itself — "Day 4" — and show the reward for day 5 greyed beside it. A streak a player cannot see is a streak that does not change what they do tomorrow.
Updated: Tested in Studio: Roblox Studio, October 2026Author: GustavoLicense: Code in this guide may be used in any Roblox experience, including a paid one
Related pack
