Appearance
Remotes and Networking
Written: 2026-10-14
Learning objectives
- You can send one-way signals with RemoteEvent.
- You can request answers with RemoteFunction.
- You can send ephemeral data with UnreliableRemoteEvent.
- You can place remotes in ReplicatedStorage.
- You can respect argument limits for remote traffic.
- You can route client-to-client effects through the server.
Prerequisites
RemoteEvent is one-way
A RemoteEvent sends a signal without waiting. The sender fires and moves on. The receiver handles it later. The remote events guide defines this as one-way communication.
Clients call FireServer. The server receives OnServerEvent with the player first, then your arguments. Servers call FireClient for one player or FireAllClients for everyone. Clients receive OnClientEvent with your arguments only.
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
signal.OnServerEvent:Connect(function(player: Player, amount: number): ()
if typeof(amount) ~= "number" then
return
end
print(player.Name)
print(amount)
end)Create a RemoteEvent named CoinSignal in ReplicatedStorage. Wait for the shared remote. Connect with player first. Type-check the intent. Print both values. Expected behavior: Output prints the sender name and amount on every client signal.
LocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
signal:FireServer(1)Wait for the same remote. Fire one intent with amount 1. The client never grants the coin itself. Expected behavior: the server handler above prints your name and 1.
Servers notify clients the other way. FireClient needs the target player. FireAllClients reaches everyone. Clients read the payload with OnClientEvent. The remote events guide shows all three directions in its quick reference.
Script in ServerScriptService
luau
--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("Notice") :: RemoteEvent
local function welcome(player: Player): ()
signal:FireClient(player, "Welcome")
end
for _, player in Players:GetPlayers() do
welcome(player)
end
Players.PlayerAdded:Connect(welcome)Create a RemoteEvent named Notice in ReplicatedStorage. Fire a welcome string to each player as they arrive. Expected behavior: each newcomer client receives one Welcome message.
LocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("Notice") :: RemoteEvent
signal.OnClientEvent:Connect(function(text: string): ()
print(text)
end)Connect the client receiver. Print the server string. Expected behavior: each client prints Welcome once after joining.
RemoteFunction is two-way and yielding
A RemoteFunction asks a question and yields for the answer. The caller pauses until the callback returns. The remote events guide defines this as two-way communication.
Clients call InvokeServer. The server answers through OnServerInvoke with player first. Keep the callback fast. Yielding callbacks delay every waiter.
Treat InvokeClient as risky. The remote events guide warns that a client error throws on the server too, a disconnect throws, and a missing return yields forever. Prefer a RemoteEvent for server-to-client updates.
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local asker = ReplicatedStorage:WaitForChild("PriceCheck") :: RemoteFunction
local function answer(player: Player, item: string): number
if item == "Apple" then
return 5
end
return 0
end
asker.OnServerInvoke = answerCreate a RemoteFunction named PriceCheck in ReplicatedStorage. Assign one callback. Branch on the item name. Return a price. Expected behavior: client callers receive 5 for apples and 0 otherwise.
LocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local asker = ReplicatedStorage:WaitForChild("PriceCheck") :: RemoteFunction
local price = asker:InvokeServer("Apple")
print(price)Invoke the server and wait. Pass one item name. Print the returned price. Expected behavior: Output prints 5.
Place remotes where both sides see them
Store remotes in ReplicatedStorage. Both server and clients can see that container. The remote events guide names ReplicatedStorage as the standard home, with Workspace or a Tool as legal alternatives for special cases.
Create each RemoteEvent and RemoteFunction in Studio with a purpose-built name. Wait for it with WaitForChild on both sides. Never trust a remote the client creates, because the server cannot see client-only objects.
Respect argument limits
Remote arguments cross the client-server boundary with strict limits. The remote events guide documents each one.
Functions arrive as nil. You cannot send behavior, only data. Tables arrive as copies, so identity checks fail across the boundary. Metatables vanish, so methods and defaults disappear. Non-string table keys convert to strings. Non-replicated Instances arrive as nil, such as a ServerStorage descendant sent to a client or a client-only part sent to the server.
LocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("OrderSignal")
signal:FireServer("Apple", 2)Send only a string and a number. Both types replicate cleanly. Expected behavior: the server receives readable values it can validate.
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("OrderSignal")
signal.OnServerEvent:Connect(function(player: Player, item: unknown, count: unknown): ()
if typeof(item) ~= "string" then
return
end
if typeof(count) ~= "number" then
return
end
print(player.Name)
end)Validate before trusting. Check both types. Silently return on bad shapes. Expected behavior: valid orders print the sender name, and malformed calls do nothing.
No client-to-client path
Clients cannot talk directly to each other. The remote events guide states this limit plainly. One client fires the server. The server validates, then relays with FireClient or FireAllClients.
Never relay blindly. Check type, permission, range, and rate before you broadcast. You learn the full gatekeeper pattern in Server Authority.
Send ephemeral data unreliably
Reliable remotes resend until delivery, which costs latency. High-frequency data that expires in one frame, like cosmetic positions or effect triggers, should skip the resend queue. UnreliableRemoteEvent fires one way without any delivery guarantee: under packet loss or congestion a fire may never arrive, so you never send purchases, grants, or state changes through it.
Payloads stay under 1000 bytes or the engine drops them. You keep each fire tiny, one position or one effect id, and you design the receiver to tolerate gaps by interpolating between arrivals. Anything that must arrive exactly once stays on RemoteEvent or RemoteFunction with an idempotency key, as the shop protocol proves in Client-Server Protocol.
Common mistakes
Mistake 1: reading OnServerEvent arguments without the player.
Wrong example below. It drops the first parameter.
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
signal.OnServerEvent:Connect(function(amount: number): ()
print(amount)
end)It prints the player object as if it were the amount. The remote events guide shows player-first ordering. Fix it by naming player first.
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
signal.OnServerEvent:Connect(function(player: Player, amount: number): ()
print(player.Name)
print(amount)
end)Mistake 2: sending a function through a remote.
Wrong example below. It tries to send behavior.
LocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
local function bonus(): ()
print("Bonus")
end
signal:FireServer(bonus)It arrives as nil because functions do not replicate. The remote events guide documents this limit. Fix it by sending data like a string or number instead.
Mistake 3: calling InvokeClient for routine updates.
Wrong example below. It yields the server on a client.
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local asker = ReplicatedStorage:WaitForChild("PriceCheck") :: RemoteFunction
local Players = game:GetService("Players")
for _, player in Players:GetPlayers() do
local answer = asker:InvokeClient(player, "Ping")
print(answer)
endIt risks a hang when the client disconnects or returns nothing. The remote events guide lists all three InvokeClient dangers. Fix it with a one-way FireClient and OnClientEvent pair.
Exercises
- Send a pickup intent from client to server with FireServer.
- Return a price with InvokeServer and OnServerInvoke.
- Announce a countdown to all clients with FireAllClients.
Solution 1
LocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
signal:FireServer(1)Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("CoinSignal")
signal.OnServerEvent:Connect(function(player: Player, amount: number): ()
if typeof(amount) ~= "number" then
return
end
print(player.Name)
end)Expected behavior: the server prints the sender name for each intent.
Solution 2
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local asker = ReplicatedStorage:WaitForChild("PriceCheck") :: RemoteFunction
asker.OnServerInvoke = function(player: Player, item: string): number
if item == "Bread" then
return 3
end
return 0
endLocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local asker = ReplicatedStorage:WaitForChild("PriceCheck") :: RemoteFunction
local price = asker:InvokeServer("Bread")
print(price)Expected behavior: the client prints 3.
Solution 3
Script in ServerScriptService
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("Countdown") :: RemoteEvent
for count = 3, 1, -1 do
signal:FireAllClients(count)
task.wait(1)
endLocalScript in StarterPlayerScripts
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local signal = ReplicatedStorage:WaitForChild("Countdown") :: RemoteEvent
signal.OnClientEvent:Connect(function(left: number): ()
print(left)
end)Expected behavior: every client prints 3, 2, 1 one second apart.
Lossy channels for ephemeral data
UnreliableRemoteEvent is the lossy sibling of RemoteEvent: asynchronous, unordered, and unreliable one-way traffic that never yields. Use it for effects and continuously changing data (positions, particles, voice-adjacent ticks) where a dropped packet is harmless. Never use it for purchases, damage, or inventory, those need the ordering and reliability of RemoteEvent or the yielding answer of RemoteFunction.
Three hard limits decide the choice. Events are not resent when lost and may arrive out of order. Client-to-server traffic throttles at roughly 500 requests per second per client, shared across all remotes of the same type. Payloads larger than 1000 bytes are dropped with an Output log noting the overage.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local fx = ReplicatedStorage:WaitForChild("FootstepFx") :: UnreliableRemoteEvent
fx.OnServerEvent:Connect(function(player: Player, position: Vector3): ()
if typeof(position) ~= "Vector3" then
return
end
print(player.Name)
end)Validate even lossy input: check the shape, then sanity-check the value (distance from the character, human speed) before relaying it to other clients.
Summary
Fire intents one way. Invoke questions two ways. Send lossy ticks over UnreliableRemoteEvent and critical acts over RemoteEvent. Store remotes where both sides see them. Send data without behavior. Relay through the server.
Production note: this chapter teaches raw RemoteEvent/RemoteFunction as the foundation. In Rojo/Wally production repos, cross-boundary traffic is owned by Comm (ServerComm/ClientComm) per the packages skill, same validation layers, one folder, one destroy path. Learn raw first, then adopt Comm in Packages. Next, enforce trust in Server Authority.