Roblox RemoteFunction Tutorial: Return Values Done Right

Last updated:

Quick answer: A RemoteFunction is Roblox’s request-reply channel: the client calls InvokeServer(), the server runs your OnServerInvoke callback, and whatever that callback returns comes back to the client as the call result — use it when the caller needs an answer, and validate everything server-side before returning it.

A RemoteFunction is the Roblox API for request-reply communication between client and server: the client calls InvokeServer(), the server runs your OnServerInvoke callback, and the callback's return value travels back to the client as the result of the call. Use a RemoteFunction when the caller needs an answer (a price, a purchase result, the current state of something), and a RemoteEvent when you only need to notify the other side.

RemoteEvent vs RemoteFunction: what's the difference?

RemoteEventRemoteFunction
Client → serverFireServer(...)InvokeServer(...) returns a value
Server → clientFireClient(...) / FireAllClients(...)InvokeClient(...) returns a value
SemanticsFire-and-forget: nobody waitsRequest-reply: the caller blocks until an answer
Typical use"I pressed attack", "an explosion happened here""What does this item cost?", "Buy this item for me"

A RemoteEvent is one-way and asynchronous: FireServer queues the message and your code keeps running immediately. A RemoteFunction is synchronous for the caller: InvokeServer yields the calling thread until the server's callback returns, exactly like a normal function call that happens to run on another machine. That single difference decides which one a feature needs — see the RemoteEvents guide for the fire-and-forget side of the picture.

How do I create my first RemoteFunction?

Put the RemoteFunction in ReplicatedStorage so both sides can see it, set OnServerInvoke on the server, and call it from a LocalScript.

-- Server Script (ServerScriptService)
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local getPrice = Instance.new("RemoteFunction")
getPrice.Name = "GetPrice"
getPrice.Parent = ReplicatedStorage

getPrice.OnServerInvoke = function(player, itemName)
	local prices = { Sword = 100, Shield = 75, Potion = 25 }
	return prices[itemName] -- nil for unknown items
end
-- LocalScript (StarterPlayerScripts)
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local getPrice = ReplicatedStorage:WaitForChild("GetPrice")
local price = getPrice:InvokeServer("Sword")
print(price) -- 100

That is the whole loop. The function you assign to OnServerInvoke receives player as an implicit first argument — Roblox prepends the calling player automatically, so never pass the player yourself — and its return value is serialized back to the client. Returning nothing gives the caller nil.

Can a RemoteFunction return multiple values — and does it yield?

Yes to both, and the yielding matters more than the return count.

-- server: returning several values at once
getPrice.OnServerInvoke = function(player, itemName)
	local prices = { Sword = 100, Shield = 75, Potion = 25 }
	local price = prices[itemName]
	local inStock = price ~= nil
	return price, inStock, "coins"
end

-- client: they arrive as multiple results
local price, inStock, currency = getPrice:InvokeServer("Sword")

While the round trip is in flight, the client thread that called InvokeServer is blocked. If the server callback yields — say it calls task.wait(2) or hits a DataStore — the client waits that whole time and its code after the call does not run until the answer arrives. That is usually what you want for a purchase ("tell me the result before I update the UI"), but two consequences follow:

Keep server callbacks short and guaranteed to return. If the work is genuinely slow, kick it off with task.spawn(), return an immediate "accepted" answer, and push the final outcome through a RemoteEvent later.

When is OnClientInvoke worth the risk?

OnClientInvoke is the mirror image: the server calls InvokeClient(player, ...) and waits for that one player's LocalScript to answer. It is rarely the right tool, because now the server yields waiting for an untrusted machine:

Reserve OnClientInvoke for genuinely client-owned answers (a screenshot-ready camera angle, a client-side graphics setting) and always wrap the call so a silent client cannot take the server down:

local function safeInvokeClient(player, ...)
	local thread = coroutine.running()
	local answered = false
	task.delay(5, function()
		if not answered then
			task.spawn(thread, nil) -- resume with a fallback instead of hanging
		end
	end)
	local result = player:InvokeClient(...)
	answered = true
	return result
end

How do I secure a RemoteFunction? (never trust the client)

The golden rule of Roblox networking: every argument from a remote is attacker-controlled input. The client can send any item name, any quantity, any price — and can call your RemoteFunction without your UI existing at all. So the server re-derives everything and checks the request against real game state.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local prices = { Sword = 100, Shield = 75, Potion = 25 }
local lastPurchase = {} -- [player] = os.clock() of their last purchase

local buyItem = Instance.new("RemoteFunction")
buyItem.Name = "BuyItem"
buyItem.Parent = ReplicatedStorage

buyItem.OnServerInvoke = function(player, itemName)
	-- 1. Validate the type and existence of every argument
	if type(itemName) ~= "string" or prices[itemName] == nil then
		return false, "Unknown item"
	end

	-- 2. Rate-limit so nobody spams purchases
	local now = os.clock()
	if lastPurchase[player] and now - lastPurchase[player] < 0.5 then
		return false, "Too fast"
	end
	lastPurchase[player] = now

	-- 3. Charge the REAL balance the server holds, never a client number
	local coins = player:GetAttribute("Coins") or 0
	local price = prices[itemName]
	if coins < price then
		return false, "Not enough coins"
	end

	player:SetAttribute("Coins", coins - price)
	return true, itemName .. " purchased"
end

Notice what the client sends: only the item name. Price comes from a server-side table, the balance lives in a server-owned attribute, and the outcome is computed entirely on the server. The client's only job is to render whatever InvokeServer returned. The same pattern applies whether coins come from attributes or leaderstats — the numbers the server trusts are the numbers the server wrote.

Common mistakes

RemoteFunction or RemoteEvent for a shop?

Most shops want both, in one division of labour: a RemoteFunction for anything the player asks ("what is the current price?", "buy this — did it work?") so the UI can show an authoritative answer, and RemoteEvents for anything the server announces ("this player's coins changed", "the weekend sale is live") which is fire-and-forget by nature. Buying is inherently request-reply — the player expects a success or failure answer — so InvokeServer returning (ok, message) is the natural fit, while the coin-change broadcast that follows stays a RemoteEvent.

Learn Luau by doing

Remotes only make sense once you have felt a client thread yield waiting for the server. On Luablox you can practise Luau interactively through /theory, drill the edge cases on /problems, and follow structured Luablox lessons that build from basic client-server calls to full request-reply gameplay systems.

Frequently asked questions

Can a RemoteEvent return a value?

No. FireServer and FireClient are fire-and-forget: nothing comes back. If the caller needs an answer, use a RemoteFunction and InvokeServer/OnServerInvoke, or keep a server-owned value in ReplicatedStorage that the client reads after the event.

Why does my RemoteFunction freeze the client?

Because InvokeServer yields the calling thread until the server answers, and there is no timeout. The usual causes are an OnServerInvoke callback that never returns (a forgotten assignment, or a yield that never resumes) or the client invoking before the server has set the callback. Assign OnServerInvoke early on the server and make sure every code path in it returns.

RemoteFunction or RemoteEvent for a shop?

RemoteFunction for the purchase itself — buying is request-reply and the player expects a success/failure answer, so InvokeServer returning (ok, message) fits. Use RemoteEvents for what the server announces afterwards (coin changes, sale broadcasts), and keep prices and balances server-side so the client cannot forge either.

Does InvokeServer block the whole client?

It blocks only the thread that called it, not the entire client. Other scripts, rendering, and input keep running — but your code after the InvokeServer call will not run until the server replies, so do not call it from threads that must stay responsive.

Is it safe to use OnClientInvoke?

Only for cosmetic, client-owned answers. The server yields waiting for an untrusted machine, so a malicious or simply lagging client can stall your server thread forever, and any value that comes back is attacker-controlled. Never let a client return value decide currency, damage, or anything that gets saved.

Explore Luau networking concepts in Roblox Studio, covering RemoteEvents, RemoteFunctions, and secure client-server replication.

Related pages