Skip to content
ENSign in

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 compareSurvives a restartSurvives a timezoneUse it when
os.time() differenceyesyesAlmost always: "24 hours since you claimed"
Calendar day (os.date("*t"))yesno — the server's UTC day, not the player'sYou want "one claim per calendar day" and accept UTC
tick() / os.clock()non/aNever for this: both reset when the server does
A running while loopnon/aNever. 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

Ultimate Script Pack

Ultimate Script Pack

$40See the pack

Get notified

Coming soon

Drop your email and we'll ping you the moment this pack goes live.