luaguides

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:

SituationUse
Player pressed a button, clicked, or performed an actionFireServer()OnServerEvent
Server needs to tell one specific player somethingFireClient()OnClientEvent
Server needs to announce something to everyoneFireAllClients()OnClientEvent
Player needs to ask the server for data and wait for itInvokeServer()OnServerInvoke
Player needs to confirm an action succeededInvokeServer() → 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 Instance object model works, which underlies everything you do with RemoteEvents and RemoteFunctions.