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.

RemoteEventRemoteFunction
Client to serverFireServer(args), server handles via OnServerEventInvokeServer(args), server returns via OnServerInvoke
Server to clientFireClient(player, args), client listens with OnClientEventRarely used; InvokeClient is dangerous
BehaviorFire-and-forget, never yieldsYields 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

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:

  1. Validate types and ranges. Check that each argument is what you expect before using it.
  2. 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.
  3. Rate limit. Exploiters fire remotes thousands of times per second. Track timestamps per player and drop or kick on spam.
  4. 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

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.

Related pages