RemoteEvents and Client-Server Communication in Roblox
In Roblox, your game runs in two places at once: the server controls the authoritative game state, and each player’s device runs a local copy of the game. Scripts on the server and scripts on each client cannot call each other directly — they operate in isolated environments. RemoteEvent and RemoteFunction are the bridges that let these two sides exchange data.
If you already know Lua syntax and want to build multiplayer features, this guide walks you through the fundamentals.
How Roblox’s Client-Server Model Works
When a player joins your game, Roblox creates a server environment that runs your Script instances in ServerScriptService. Each player’s device runs a separate client environment that runs their LocalScript instances. Neither side can see the variables or call the functions of the other side directly.
This separation protects your game from cheaters. If clients could directly modify leaderstats values, exploiters would give themselves infinite gold. The server is the source of truth, and communication between sides must go through explicitly defined channels.
RemoteEvent and RemoteFunction live in ReplicatedStorage, which is accessible from both server and client scripts. This is where you create them — either in Roblox Studio’s Explorer or programmatically at runtime.
RemoteEvent: Fire and Forget
A RemoteEvent sends a one-way message across the client-server boundary. The sender does not wait for a response. Use it when the client needs to report an action to the server, or when the server needs to notify clients about something.
Sending from Client to Server
On the client side, a LocalScript fires the event:
-- In a LocalScript (client-side)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("PlayerAction")
-- Fire the event to the server with some data
remoteEvent:FireServer("jump", 150)
On the server side, a Script listens for that event:
-- In a Script (server-side)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:WaitForChild("PlayerAction")
remoteEvent.OnServerEvent:Connect(function(player, action, value)
-- player is the Roblox Player object who sent this
print(player.Name .. " performed " .. action .. " with value " .. value)
end)
OnServerEvent passes the Player object as its first argument, followed by whatever arguments the client sent. The server always knows who triggered the event.
Sending from Server to Client
The server fires to a specific player like this:
-- In a Script (server-side)
local player = -- get a player somehow
remoteEvent:FireClient(player, "notification", "You earned a badge!")
The client listens the same way it listens for client-to-server events, using OnClientEvent:
-- In a LocalScript (client-side)
local remoteEvent = ReplicatedStorage:WaitForChild("PlayerAction")
remoteEvent.OnClientEvent:Connect(function(messageType, message)
if messageType == "notification" then
print("Server notification: " .. message)
end
end)
To broadcast to every connected client at once, use FireAllClients:
remoteEvent:FireAllClients("event", "A new round is starting!")
RemoteFunction: Request and Response
A RemoteFunction lets the client request data from the server and wait for a response. When a client calls InvokeServer, the script yields until the server sends back a return value.
Server-Side Handler
On the server, you assign a callback to OnServerInvoke:
-- In a Script (server-side)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteFunction = ReplicatedStorage:WaitForChild("GetPlayerData")
remoteFunction.OnServerInvoke = function(player, dataKey)
-- Validate the request on the server side
if typeof(dataKey) ~= "string" then
return nil
end
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats then
local stat = leaderstats:FindFirstChild(dataKey)
if stat then
return stat.Value
end
end
return nil
end
Client-Side Call
The client invokes the server and gets a return value:
-- In a LocalScript (client-side)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteFunction = ReplicatedStorage:WaitForChild("GetPlayerData")
-- This line yields until the server responds
local goldValue = remoteFunction:InvokeServer("Gold")
print("Player gold:", goldValue)
Because InvokeServer yields the script, do not call it from code that runs every frame, like RenderStepped. If you need to send frequent requests, use a RemoteEvent instead and have the server push data back via FireClient.
Also note: if no handler is connected on the server, the client yields forever. Always connect OnServerInvoke before the client tries to invoke it.
When to Use Which
Here is a quick decision guide based on your use case:
| Situation | Use |
|---|---|
| Player pressed a button, clicked, or performed an action | FireServer() → OnServerEvent |
| Server needs to tell one specific player something | FireClient() → OnClientEvent |
| Server needs to announce something to everyone | FireAllClients() → OnClientEvent |
| Player needs to ask the server for data and wait for it | InvokeServer() → OnServerInvoke |
| Player needs to confirm an action succeeded | InvokeServer() → return a status |
RemoteEvent is simpler and more common. Start there. Only reach for RemoteFunction when you genuinely need a response from the server.
Security: Never Trust the Client
This is the most important habit in Roblox networking. Anything a client sends to the server can be manipulated by an exploiter. Treat all input as hostile until proven otherwise.
Consider a naive handler that awards gold based on what the client sends:
-- DANGEROUS: trusting the client directly
remoteEvent.OnServerEvent:Connect(function(player, goldAmount)
player.leaderstats.Gold.Value = goldAmount
end)
An exploiter can fire this event with any number. Instead, validate the action server-side and apply your own logic:
-- SAFE: server decides the reward based on its own game rules
remoteEvent.OnServerEvent:Connect(function(player, action)
if action == "defeatEnemy" then
local currentGold = player.leaderstats.Gold.Value
player.leaderstats.Gold.Value = currentGold + 50
end
end)
Always check types with typeof(), validate ranges, and make sure the action is even possible from the server’s perspective.
Rate Limiting
Roblox silently drops RemoteEvent fires from a client if they fire too many in a short window. This is a anti-exploit measure, but it also means you should not send an event every frame from the client. If you need high-frequency updates, have the client send them in batches or use a heartbeat-based throttle.
-- Throttle events to at most once per half second
local lastFire = 0
local THROTTLE = 0.5
game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
lastFire = lastFire + deltaTime
if lastFire >= THROTTLE then
lastFire = 0
remoteEvent:FireServer("positionUpdate", workspace.CurrentCamera.CFrame)
end
end)
BindableEvents: Server-Side Communication
BindableEvent and BindableFunction look similar to their Remote counterparts, but they only work within the same side of the boundary. They cannot send data across the network.
Use them when two server scripts need to communicate without direct references:
-- In a ModuleScript stored in ServerScriptService
local PlayerDied = Instance.new("BindableEvent")
return PlayerDied
-- In Script A (dealing damage)
local PlayerDied = require(path.to.ModuleScript)
PlayerDied:Fire(playerWhoDied)
-- In Script B (handling death effects)
local PlayerDied = require(path.to.ModuleScript)
PlayerDied.Event:Connect(function(player)
print(player.Name .. " has died")
-- spawn death screen, respawn timer, etc.
end)
This keeps your server code decoupled. If you need something to work across the network, you must use RemoteEvent or RemoteFunction.
Setting Up a Complete Example
Here is a practical setup that creates a RemoteEvent programmatically, sets up a player action on the client, and handles it securely on the server.
First, the server creates the RemoteEvent and sets up the handler:
-- In a Script (ServerScriptService)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local playerAction = Instance.new("RemoteEvent")
playerAction.Name = "PlayerAction"
playerAction.Parent = ReplicatedStorage
playerAction.OnServerEvent:Connect(function(player, action, value)
if action == "vote" then
-- Validate vote value
if value ~= "yes" and value ~= "no" then return end
print(player.Name .. " voted " .. value)
elseif action == "purchase" then
-- Server-side purchase logic
if player.leaderstats.Coins.Value >= 100 then
player.leaderstats.Coins.Value = player.leaderstats.Coins.Value - 100
playerAction:FireClient(player, "purchaseResult", "success")
else
playerAction:FireClient(player, "purchaseResult", "insufficient_funds")
end
end
end)
Then the client sends actions and listens for responses:
-- In a LocalScript (StarterPlayerScripts)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local playerAction = ReplicatedStorage:WaitForChild("PlayerAction")
-- Send a vote to the server
playerAction:FireServer("vote", "yes")
-- Listen for purchase result
playerAction.OnClientEvent:Connect(function(resultType, result)
if resultType == "purchaseResult" then
print("Purchase result: " .. result)
end
end)
Notice how the server always validates the input, and how the server fires back to the specific client using FireClient to confirm the purchase result.
Conclusion
RemoteEvent and RemoteFunction are the two pillars of Roblox networking. Use RemoteEvent for fire-and-forget communication in either direction. Use RemoteFunction when the client needs to wait for a server response. In both cases, validate everything on the server — never assume the client is telling the truth.
Once you are comfortable with these basics, look into BindableEvent for decoupling your server code, and explore how RemoteFunction callbacks can be structured to handle asynchronous server operations cleanly.
For next steps, try building a simple chat system where players send messages through a RemoteEvent, or a stat-fetching feature using RemoteFunction to replace static RemoteEvents with dynamic server queries.
See Also
- Roblox Lua: Getting Started — Set up your Roblox development environment and write your first Lua scripts if you are new to Roblox.
- Roblox UI Scripting — Learn how to create and manipulate on-screen interfaces, which pairs well with RemoteEvents for communicating UI state between server and client.
- Scripting Objects in Roblox — Dive deeper into how Roblox’s
Instanceobject model works, which underlies everything you do with RemoteEvents and RemoteFunctions.