Master Remote Events in Roblox Scripting: Build Secure Multiplayer Features
Remote Events in Roblox scripting let a client and the server send each other messages without pausing to wait for a reply. Use them for requests such as “try to buy this item” or for server-approved updates such as “show this effect to nearby players.” The important design rule is that a RemoteEvent carries a message; it does not make the request trustworthy. The server must decide whether an action is allowed and update important game state itself.
What a RemoteEvent does
A RemoteEvent is an asynchronous, one-way communication object. The sender calls a method to fire it, and the receiving side runs a connected event handler. A RemoteEvent call does not return a value. It is reliable and ordered while the recipient remains connected, but it is not a request-and-response mechanism.
For a shared event, a common location is ReplicatedStorage, where both the server and clients can access it. The remote can live elsewhere when the design calls for it, such as within a Tool, but both endpoints must be able to see the instance. Clients cannot send messages directly to other clients: a client can notify the server, and the server can then choose which clients receive a follow-up event.
| Direction | Sender method | Receiver | Typical use |
| Client to server | FireServer(...) | OnServerEvent | Request an action or submit player input |
| Server to one client | FireClient(player, ...) | OnClientEvent | Send a private notification or update |
| Server to every connected client | FireAllClients(...) | OnClientEvent | Broadcast a shared effect or announcement |
When a client fires a RemoteEvent, Roblox supplies that player's Player object as the first argument to the server's OnServerEvent callback. Do not try to pass or choose that argument yourself.
Build a secure item-purchase event
This four-part example connects a GUI button to server-owned purchase logic. In Explorer, create a RemoteEvent in ReplicatedStorage and name it PurchaseItem. Add a LocalScript under a TextButton or ImageButton. Add a normal Script in ServerScriptService. If you also use the effect example below, create a second RemoteEvent named EffectEvent in ReplicatedStorage. The example assumes your game already creates a server-maintained leaderstats.Cash value and has its own inventory-granting code.
Step 1: Send only the player's intent
The client asks to purchase an item by sending its catalog ID. The cross-platform Activated event works for mouse, touch, and gamepad input on a GUI button.
-- LocalScript parented to a TextButton or ImageButton
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local purchaseItem = ReplicatedStorage:WaitForChild("PurchaseItem")
local button = script.Parent
button.Activated:Connect(function()
purchaseItem:FireServer("SmallHealthPotion")
end)
The client does not send the price, its cash balance, or an instruction to grant itself an item. Those values and decisions belong to the server.
Step 2: Validate and process the request on the server
Keep the item list and prices in a server Script or server-only module. The following handler checks the request type, accepts only a known catalog entry, applies a per-player cooldown, and checks the server's cash value before deducting anything.
-- Script in ServerScriptService
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local purchaseItem = ReplicatedStorage:WaitForChild("PurchaseItem")
local catalog = {
SmallHealthPotion = { price = 25 },
}
local lastRequestAt = {}
local COOLDOWN_SECONDS = 0.25
purchaseItem.OnServerEvent:Connect(function(player, itemId)
if typeof(itemId) ~= "string" or #itemId > 40 then
return
end
local item = catalog[itemId]
if not item then
return
end
local now = os.clock()
local previous = lastRequestAt[player.UserId] or 0
if now - previous < COOLDOWN_SECONDS then
return
end
lastRequestAt[player.UserId] = now
local leaderstats = player:FindFirstChild("leaderstats")
local cash = leaderstats and leaderstats:FindFirstChild("Cash")
if not cash or not cash:IsA("IntValue") or cash.Value < item.price then
return
end
cash.Value = cash.Value - item.price
-- Grant the item through your server-owned inventory system here.
end)
Players.PlayerRemoving:Connect(function(player)
lastRequestAt[player.UserId] = nil
end)
Adapt the value check to the value object or currency system your game uses. If a purchase depends on a shop's location, the player's alive state, a quest, or an unlock, check that condition on the server too. Keep the validation and the state change together, and avoid yielding between checking the balance and deducting the price.
Step 3: Let the server choose the outcome
For actions other than purchases, follow the same pattern: the client reports input, and the server validates it before changing shared state. For example, after a server script confirms a spell is unlocked and the target position is valid, it can broadcast an effect position:
-- Server Script, after validating the action
local effectEvent = ReplicatedStorage:WaitForChild("EffectEvent")
effectEvent:FireAllClients(serverApprovedPosition)
Use FireClient(player, ...) when only one player should receive an update. Use FireAllClients(...) when all connected clients need it. To omit a particular player, explicitly fire to the intended recipients; FireAllClientsExcept() is not a RemoteEvent method.
Step 4: Render client-only feedback
A LocalScript can listen for the approved update and display a particle burst, sound, or other temporary presentation. Keep visual-only effects on the client when they do not need to affect gameplay.
-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local effectEvent = ReplicatedStorage:WaitForChild("EffectEvent")
effectEvent.OnClientEvent:Connect(function(position)
if typeof(position) ~= "Vector3" then
return
end
-- Play a local visual effect at the server-approved position.
end)
This example checks the received type defensively. The client should not decide that the spell hit, award damage, or create a reward; those are server decisions.
RemoteEvent, RemoteFunction, or UnreliableRemoteEvent?
| Object | Communication | Choose it when |
RemoteEvent | Asynchronous, one-way; no response is returned | You need to send an action request, notification, or update |
RemoteFunction | Two-way; the caller yields until it gets a response | The caller genuinely needs a return value before continuing |
UnreliableRemoteEvent | Asynchronous, one-way, with no delivery or order guarantee | Frequent, noncritical data can be dropped without harming game state |
Prefer a RemoteEvent when the sender does not need a response. A RemoteFunction can hold up the calling thread while it waits, and invoking a client from the server adds risks if that client errors, disconnects, or never returns. UnreliableRemoteEvents suit transient updates such as rapidly changing visual or aim data, not purchases, inventory, or other state that must arrive in order.
RemoteEvent security checklist
- Validate every client argument on the server. Check type, size, range, and structure before using the data.
- Use an allowlist. Map a short item or action ID to server-owned settings. Do not let a client provide a price, asset path, arbitrary Instance, or code to run.
- Check permission and context. Verify distance to a shop, character state, cooldown, available resources, and any required unlock on the server.
- Rate-limit server work. A client-side debounce can improve the interface, but exploiters can bypass it. Enforce a server-side limit for operations that can be spammed or consume resources.
- Make the server a gatekeeper. Validate a client-triggered effect before relaying it to other players; do not blindly broadcast client-supplied positions.
Roblox's security guidance treats remote input as untrusted. Server validation is essential even when your LocalScript already checks the same values. For high-value purchases or persistent inventory, use the same server-authoritative checks in your data-saving and transaction logic.
Test and troubleshoot your RemoteEvents
- “RemoteEvent not found”: Check the name and parent in Explorer, and use
WaitForChild() when a script needs to wait for replication.
- The handler never runs: Confirm the firing code is in a LocalScript, the receiving code is in a server Script, and both point to the same RemoteEvent instance.
- The server rejects a purchase: Inspect the server-side item ID, currency object, and permission checks. Do not fix a mismatch by trusting a client-supplied price.
- Effects appear only for one player: Check whether you used
FireClient() instead of FireAllClients(), or whether each client has an OnClientEvent listener.
- Messages behave unexpectedly: Connect the listener before firing. Roblox queues messages briefly when no listener is connected, but that queue is limited. If correctness depends on the order of RemoteEvent and RemoteFunction calls, add your own sequencing; Roblox does not guarantee their relative order.
Use Studio's Server & Clients test mode to run a server with multiple simulated players. Check the Output window on both sides, then test join and leave behavior and try invalid inputs. Studio's client-server test is a better check for multiplayer behavior than relying only on a solo play session.
Official Roblox references