Roblox RemoteEvents Explained: Client–Server Communication
Quick answer: A RemoteEvent lets the client ask the server (or the server tell the client) to do something: call :FireServer() from a LocalScript, handle it with OnServerEvent on the server, and always validate the arguments there.
A RemoteEvent is a Roblox object that lets scripts on the client and scripts on the server send messages to each other. The client creates the request with :FireServer(), and a server script receives it through the :OnServerEvent callback — this is the only sanctioned way for a player's device to ask the server to do something.
Why Roblox has a client-server boundary
Every Roblox game runs two copies of your logic. Each player's device runs client code (LocalScripts) that handles input, camera, UI, and visual effects. Roblox's servers run server code (Scripts) that owns game state: player data, currency, physics authority, and win conditions.
The boundary exists for security. Clients are untrusted — anything on a player's device can be read, modified, or faked by an exploiter. FilteringEnabled is always on in modern Roblox, which means changes made by one client are never replicated to other players or the server. If a LocalScript sets a player's coins to 999,999, nothing actually happens on the server. That is exactly why RemoteEvents matter: they give you a narrow, auditable channel across the boundary instead of letting clients write state directly.
Where to put RemoteEvents: ReplicatedStorage
A RemoteEvent must exist in a container both sides can see. The standard placement is ReplicatedStorage, because it replicates to clients but is not directly accessible from outside the game. Create it once (for example, in ServerStorage during development, then move it, or just place it in ReplicatedStorage in Studio) and reference it by path:
-- Shared module used by both sides
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Remotes = ReplicatedStorage:WaitForChild('Remotes')
local CoinCollected = Remotes:WaitForChild('CoinCollected')
Use WaitForChild rather than dot indexing when loading remotes on the client, since replication timing means the object may not have arrived yet when a LocalScript starts.
Server-only secrets belong in ServerStorage, which clients can never see. Never put sensitive values in ReplicatedStorage "temporarily" — assume everything replicated to the client is public.
RemoteEvent vs RemoteFunction
RemoteEvents fire messages without waiting for a reply; RemoteFunctions make request-response calls that yield until the other side returns a value.
| RemoteEvent | RemoteFunction | |
|---|---|---|
| Client to server | FireServer(args), server handles via OnServerEvent | InvokeServer(args), server returns via OnServerInvoke |
| Server to client | FireClient(player, args), client listens with OnClientEvent | Rarely used; InvokeClient is dangerous |
| Behavior | Fire-and-forget, never yields | Yields until a result comes back |
Prefer RemoteEvents for almost everything: pickups, chat commands, UI actions, notifications. Use a RemoteFunction only when you truly need a return value, such as fetching a shop price. A hanging or erroring OnServerInvoke will freeze the calling thread indefinitely, so always guard InvokeServer calls.
Client to server: FireServer and OnServerEvent
The client fires; the server listens. When the server callback runs, Roblox automatically prepends the sending Player as the first argument — you never pass it yourself, and exploiters cannot spoof it.
-- LocalScript (client)
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local CoinCollected = ReplicatedStorage.Remotes.CoinCollected
CoinCollected:FireServer(coinInstance)
-- Script in ServerScriptService (server)
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local CoinCollected = ReplicatedStorage.Remotes.CoinCollected
CoinCollected.OnServerEvent:Connect(function(player, coin)
-- player is injected by the engine
print(player.Name .. ' touched a coin:', coin)
end)
Server to client: FireClient and OnClientEvent
The server targets specific players with :FireClient(player, ...), or everyone with :FireAllClients(...). The client listens with OnClientEvent inside a LocalScript:
-- Server: notify one player their balance changed
coinsChanged:FireClient(player, newBalance)
-- LocalScript (client)
coinsChanged.OnClientEvent:Connect(function(newBalance)
coinLabel.Text = tostring(newBalance)
end)
Argument rules you must know
- Only serializable types cross the boundary: strings, numbers, booleans, nil, tables of those, Instances, BrickColors, Vector3, CFrame, Color3, EnumItem, and similar value types. Functions, metatables (the metatable itself is stripped), userdata beyond the supported list, and mixed tables cannot be sent.
- Tables arrive as plain dictionaries/arrays. If you send an OOP instance built with setmetatable, only its raw fields survive — methods and the metatable are lost.
- The Player argument on the server is engine-injected and cannot be forged, so it is safe to trust who sent the message — never what they sent.
- Every extra argument after the player on OnServerEvent comes straight from the client and is attacker-controlled.
Security essentials: never trust the client
Because any client can call any remote with any arguments at any time, treat every remote handler like a public web API endpoint:
- Validate types and ranges. Check that each argument is what you expect before using it.
- Re-check state on the server. Distance, cooldowns, ownership, and currency must all be verified server-side. The client asking is not proof anything happened.
- Rate limit. Exploiters fire remotes thousands of times per second. Track timestamps per player and drop or kick on spam.
- Never echo sensitive data. Do not design remotes that hand out other players' stats or accept a "which player" argument as authorization.
Here is a complete, hardened coin-pickup flow:
-- Server: ServerScriptService/CoinService.lua
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Players = game:GetService('Players')
local CoinCollected = ReplicatedStorage.Remotes.CoinCollected
local COOLDOWN = 0.25
local lastPickup = {} -- [player] = os.clock()
local function onCoinCollected(player, coin)
-- Type validation: reject garbage immediately
if typeof(coin) ~= 'Instance' or not coin:IsA('BasePart') then
return
end
if not coin:GetAttribute('CoinValue') then
return -- not a real coin
end
-- Rate limiting
local now = os.clock()
if lastPickup[player] and now - lastPickup[player] < COOLDOWN then
return
end
lastPickup[player] = now
-- Server-side distance check: was the player actually near it?
local character = player.Character
local root = character and character:FindFirstChild('HumanoidRootPart')
if not root or (root.Position - coin.Position).Magnitude > 12 then
return
end
local value = math.floor(tonumber(coin:GetAttribute('CoinValue')) or 0)
if value <= 0 or value > 100 then
return
end
-- Authoritative state change happens ONLY here
local leaderstats = player:FindFirstChild('leaderstats')
local coins = leaderstats and leaderstats:FindFirstChild('Coins')
if coins then
coins.Value += value
end
coin:Destroy()
end
CoinCollected.OnServerEvent:Connect(onCoinCollected)
Players.PlayerRemoving:Connect(function(player)
lastPickup[player] = nil
end)
-- Client: LocalScript inside StarterPlayerScripts
local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local CoinCollected = ReplicatedStorage.Remotes.CoinCollected
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
for _, part in character:GetChildren() do
if part:IsA('BasePart') then
part.Touched:Connect(function(otherPart)
if otherPart:GetAttribute('CoinValue') then
CoinCollected:FireServer(otherPart)
end
end)
end
end
Even though the client fires the remote, the server decides everything: whether the coin existed, whether the player was close enough, how much it paid, and when to destroy it. An exploiter spamming the remote earns at most one coin per cooldown window, and only while standing near real coins.
Common mistakes with RemoteEvents
- Doing gameplay logic in LocalScripts because it is easier to test. It works on your screen and nowhere else.
- Passing the player manually (
:FireServer(player)). The server already gets the true sender; passing it yourself just adds another untrusted argument. - Trusting position arguments. If the server needs to know where something happened, verify against server-known positions rather than accepting coordinates.
- Firing per-frame. High-frequency remotes waste bandwidth; batch updates or drive continuous behavior locally and sync occasionally. For timing-sensitive loops, compare task.wait and task.spawn patterns in task.wait vs wait.
- Using RemoteFunction where an event suffices, creating yield chains that stall under lag.
Learn Luau by doing
Reading about remotes is one thing; wiring them up against a live server check is another. Brush up on the language fundamentals in Luau theory, practice the exact validation patterns above in guided problems, and follow structured Luablox lessons that build client-server games step by step.
Frequently asked questions
What is a RemoteEvent in Roblox?
A RemoteEvent is an object that carries messages between client scripts and server scripts. The client calls FireServer() to send data to the server, which receives it through OnServerEvent, while the server can target players with FireClient().
Where should I put RemoteEvents?
Place them in ReplicatedStorage so both the server and clients can access them. Server-only objects belong in ServerStorage, which clients can never read.
What is the difference between FireServer and FireClient?
FireServer is called by the client to send a message to the server; the server handles it in OnServerEvent with the sending Player automatically passed as the first argument. FireClient is called by the server to target a specific player, whose LocalScript receives it via OnClientEvent.
Can exploiters fake RemoteEvent arguments?
Yes. Everything except the auto-injected Player argument comes from the client and can be spoofed, so servers must validate types, re-check distances and cooldowns, and rate limit every remote handler.
Should I use RemoteEvent or RemoteFunction?
Use a RemoteEvent for fire-and-forget messages such as pickups and UI updates. Use a RemoteFunction only when you need a return value, because InvokeServer yields until the server responds and can hang if OnServerInvoke errors.
Explore Luau networking concepts in Roblox Studio, covering RemoteEvents, RemoteFunctions, and secure client-server replication.