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?
| RemoteEvent | RemoteFunction | |
|---|---|---|
| Client → server | FireServer(...) | InvokeServer(...) returns a value |
| Server → client | FireClient(...) / FireAllClients(...) | InvokeClient(...) returns a value |
| Semantics | Fire-and-forget: nobody waits | Request-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:
- Never call
InvokeServerfrom a thread that must keep animating or rendering; the caller stalls, so fire the request from a task and update the UI when it returns. - There is no timeout. A callback that never returns — an early
returnforgotten inside a loop, or a yield that never resumes — leaves the client thread waiting forever, which users experience as the whole script freezing.
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:
- An exploiter can simply never answer, parking your server thread forever. A busy server with a few of these stalls is a denial-of-service you wrote yourself.
- Whatever comes back is client-controlled. A client that returns
999999for "my cosmetic particle count" is only a problem if you believed it — so never use client return values for anything that affects gameplay, currency, or saving. - Even honest clients can lag or leave mid-call.
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
- Setting
OnServerInvoketoo late — or never. The callback must be assigned on the server, early. If the client invokes before the callback exists, the call simply waits for a reply that is never generated, and the client thread freezes with no error. - Infinite yield on
WaitForChild("SomeRemote"). The name in the LocalScript does not match the instance, the remote lives in a server-only container, or it is created locally on the client (client-created instances never replicate to the server). Keep remotes in ReplicatedStorage and share one name constant. If this is your exact symptom, the infinite yield fix guide walks through it. - Calling
FireServeron a RemoteFunction orInvokeServeron a RemoteEvent. The methods are not interchangeable — the wrong pairing throws immediately. Fire-and-forget is a RemoteEvent; ask-and-answer is a RemoteFunction. - Forgetting that
InvokeServeryields. Calling it inside a tight loop or aRenderSteppedhandler stalls that thread on every call. - Returning large tables on every call. Everything you return is serialized over the network; return only the delta the client needs.
- Treating a nil return as an error. A callback with no return statement answers
nil; that is a successful call with an empty answer, not a failure.
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.