How to efficiently get time across server/client

I’ve learned to always use workspace:GetServerTimeNow() to get consistent times across server and client. So daily rewards, etc, anything that needs to store a time that something occurred on the server and client needs to display.

I’ve since started doing os.time()/os.clock() on server and workspace:GetServerTimeNow() on client, as they return more or less the same number (os.time() just floored and the decimals may not be millisecond accurate across both, but I’m not after millisecond accuracy when we talking hour/day long timers)

Is there a better method though? As I know workspace:GetServerTimeNow() is expensive. I just want a clean/effective way to get time. if the server says this thing happened at 1756093129, then I want a way for client to be able to do

clientTime - serverTime = timeElapsed

and with os.time() varying across client/server, timeElapsed could end up being hours off

Note to add, wish Roblox would make os.time() return differing results when testing in studio :melting_face: don’t know how many times I’ve used and everything’s been great in studio, only to go play in live game and all of a sudden client and server have vastly different numbers

I don’t have an exact answer, but I want to know why you’re doing this. Does it have to do with latency?

Here’s something random, there’s also workspace.DistributedGametime, if that helps for your purposes.

You really should be calculating delta time from (os.time() - startTime). I can’t really think of a reason why these would be hours off unless you were slowly adding the delta time.

After testing it looks like the client and server are around a half second delayed from each other for some reason but that shouldn’t be an issue.

cc @EffBeeCee
Let’s say daily rewards

Player claims the reward, we save the time they claimed it on the server using

-- set and saved on the server to players data
Data.ClaimedReward = os.time()

Now on the client we want to get how long before they can claim their next days rewards

local DAY = 24 * 60 * 60
local TimeLeft = (Data.ClaimedReward + DAY) - workspace:GetServerTimeNow()

This works. If I did

local TimeLeft = (Data.ClaimedReward + DAY) - os.time()

on the client, it could be any sort of time, as os.time() is different on the client and can be manipulated by just setting your systems time

this is server based, my concern is tracking across any server, players leaving/rejoining later/etc. needing to display correct time as mentioned in example above ^ and is also a different number across client/server

Why don’t you just use a RemoteFunction, have the client ask the server what workspace:GetServerTimeNow() is. If they’re desperate enough to make it appear to be claimed, who cares, they’re not actually claiming it.

It really doesn’t matter if the client time is manipulated because if this is for visual properties (which it should be), an exploiter won’t care about this anyways. os.time() is not different on the client unless hacked by the way.

1 Like

Calling workspace:GetServerTimeNow() every frame shouldn’t be too expensive, it should only take a tiny fraction of a microsecond for the call. Just cache the result for one frame for the computations you need it for if performance is a concern.

I do a lot of work with networking. No, I don’t think there is a better way of doing it, and it’s not that expensive either. 99% of the time Roblox’s methods will be way more optimized than what LuaU can achieve.

Additionally, if you’re looking to work out the latency, then you can send the server time in the remote call, such as

--Server
Remote:FireAllClients(workspace:GetServerTimeNow())
Remote.OnClientEvent:Connect(function(timeSent)
    local latency = workspace:GetServerTimeNow() - timeSent
end
1 Like

Not sure if this is the kind of stuff you’re looking for but this is the logic I use for my daily offer code. Both the client and server use this module to determine which item is included in the daily offer.

Module
local DailyOffer = {}

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local itemData = require(ReplicatedStorage.SharedModules.itemData)
local getDictSize = require(ReplicatedStorage.SharedModules.getDictSize)

local currentDaysFromEpoch
local epoch = 1715634000 -- 13 May 2024 3pm MST

function DailyOffer.getDaysFromEpoch()
	return (os.time() - epoch)/60/60/24
end

local defaults = {"Defualt", "Cheer", "Impact"}
local function getRares(itemType)
	local data = {}
	for itemName, itemInfo in pairs(itemType) do
		if itemInfo.Rarity == "Rare" and not table.find(defaults, itemName) then
			data[itemName] = itemInfo
		end
	end
	return data
end

local function getOfferedItem()
	local days = math.floor(DailyOffer.getDaysFromEpoch()) print(days)
	local types = {"BallTrails", "Celebrations", "TackleEffects"}
	local selectedType = types[days % 3 + 1] -- +1 bc arrays start at 1 instead of 0

	local count = 0
	local items = getRares(itemData[selectedType])
	local index = days % getDictSize(items)
	for itemName in pairs(items) do
		if count == index then
			return selectedType, itemName
		end
		count += 1
	end
end

local function updateOffer()
	currentDaysFromEpoch = math.floor(DailyOffer.getDaysFromEpoch())
	DailyOffer.itemType, DailyOffer.itemName = getOfferedItem()

	local offerValueObj = ReplicatedStorage.ReplicatedValues.DailyOffer
	offerValueObj.Value = DailyOffer.itemName
	offerValueObj.Type.Value = DailyOffer.itemType
end

local function checkForNewDay()
	local newDaysFromEpoch = math.floor(DailyOffer.getDaysFromEpoch())
	if newDaysFromEpoch ~= currentDaysFromEpoch then
		updateOffer()
		ReplicatedStorage.Networking.DailyOfferChanged:FireAllClients()
	end
	task.delay(1, checkForNewDay)
end

function DailyOffer.init() -- Called once by the server.
	updateOffer()
	checkForNewDay()
end

return DailyOffer