# Roblox Remote Events

> Use when implementing client-server communication in Roblox, firing events between LocalScripts and Scripts, passing data across the network boundary, syncing game state, or defending against exploits that abuse RemoteEvents or RemoteFunctions.

- Skill: `sentinelcore/roblox-remote-events` (Agent Skill)
- Install (CLI): `npx skillmds add sentinelcore/roblox-remote-events`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sentinelcore/roblox-remote-events/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: sentinelcore (https://skillmd.com/u/sentinelcore)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/sentinelcore/roblox-remote-events

---


# Roblox Remote Events & Functions

## RemoteEvent vs RemoteFunction

| Type | Direction | Returns value? | Use when |
|---|---|---|---|
| `RemoteEvent` | Any direction | No (fire-and-forget) | Notifying server of player action, broadcasting state |
| `RemoteFunction` | Client→Server | Yes (yields caller) | Client needs a result back (e.g. fetch inventory) |
| `UnreliableRemoteEvent` | Any direction | No | High-frequency updates where dropped packets are fine |

**Default to RemoteEvent.** Avoid server→client `RemoteFunction` — an exploiter's frozen callback stalls your server thread indefinitely.

---

## Where to Put Remotes

Always store Remotes in `ReplicatedStorage`. Create them from a server Script that runs before any LocalScript.

```
ReplicatedStorage/
  Remotes/
    DealDamage        (RemoteEvent)
    GetInventory      (RemoteFunction)
    SyncPosition      (UnreliableRemoteEvent)
```

```lua
-- Script in ServerScriptService
local folder = Instance.new("Folder")
folder.Name = "Remotes"
folder.Parent = game:GetService("ReplicatedStorage")

local function make(class, name)
    local r = Instance.new(class)
    r.Name = name
    r.Parent = folder
    return r
end

make("RemoteEvent",           "DealDamage")
make("RemoteFunction",        "GetInventory")
make("UnreliableRemoteEvent", "SyncPosition")
```

---

## Firing Patterns

### Client → Server (FireServer)

```lua
-- LocalScript
local DealDamage = game:GetService("ReplicatedStorage").Remotes:WaitForChild("DealDamage")
DealDamage:FireServer({ targetId = 12345, amount = 50 })
-- First arg on server is always the firing Player (injected automatically, cannot be spoofed)
```

```lua
-- Script (server) — VALIDATE everything in the payload
DealDamage.OnServerEvent:Connect(function(player, data)
    -- player identity is trustworthy; data contents are not
end)
```

### Server → One Client

```lua
local Notify = game:GetService("ReplicatedStorage").Remotes:WaitForChild("Notify")
Notify:FireClient(player, { message = "Welcome!" })
```

```lua
-- LocalScript
Notify.OnClientEvent:Connect(function(data)
    print(data.message)
end)
```

### Server → All Clients

```lua
AnnounceEvent:FireAllClients({ text = "Game starting in 10 seconds!" })
```

### RemoteFunction (Client Calls, Server Returns)

```lua
-- Script (server)
GetInventory.OnServerInvoke = function(player)
    return getPlayerInventory(player.UserId)
end
```

```lua
-- LocalScript
local inventory = GetInventory:InvokeServer()  -- yields until server returns
```

### UnreliableRemoteEvent (High-Frequency Sync)

```lua
-- LocalScript
RunService.Heartbeat:Connect(function()
    SyncPosition:FireServer(character.HumanoidRootPart.CFrame)
end)
```

```lua
-- Script (server) — still validate
SyncPosition.OnServerEvent:Connect(function(player, cframe)
    if typeof(cframe) ~= "CFrame" then return end
    -- apply with sanity bounds check
end)
```

---

## CRITICAL: Server-Side Security

**The client is hostile. Treat every argument as untrusted input.**

```lua
local MAX_DAMAGE = 100
local COOLDOWNS = {}
local COOLDOWN_SECONDS = 0.5

DealDamage.OnServerEvent:Connect(function(player, data)
    -- 1. Rate limit
    local now = tick()
    if COOLDOWNS[player.UserId] and now - COOLDOWNS[player.UserId] < COOLDOWN_SECONDS then
        return
    end
    COOLDOWNS[player.UserId] = now

    -- 2. Type checks
    if type(data) ~= "table" then return end
    if type(data.targetId) ~= "number" then return end
    if type(data.amount) ~= "number" then return end

    -- 3. Range clamp
    local amount = math.clamp(data.amount, 0, MAX_DAMAGE)

    -- 4. Server-side weapon lookup — never trust client-provided Instance
    local weapon = getEquippedWeapon(player)
    if not weapon then return end

    -- 5. Server-side target lookup
    local target = getPlayerByUserId(data.targetId)
    if not target then return end

    applyDamage(target, amount, player)
end)
```

---

## Exploit Patterns & Defenses

| Exploit | What the attacker does | Defense |
|---|---|---|
| Argument injection | Sends unexpected types to crash handler | Type-check all arguments |
| Damage amplification | Sends `amount = math.huge` | Clamp to sane maximum |
| Remote spam | Fires thousands of times per second | Per-player cooldown |
| Spoofed target | Sends another player's UserId | Server resolves from its own state |
| Infinite yield | Never returns from `OnClientEvent` callback | Avoid server→client RemoteFunction |
| Duplicate action | Replays a valid fire to buy twice | Check state / consume token before acting |

---

## Quick Reference

```
FireServer(args)            LocalScript → server
FireClient(player, args)    server → one client
FireAllClients(args)        server → every client
InvokeServer(args)          LocalScript → server, waits for return
OnServerEvent               server-side listener for FireServer
OnClientEvent               client-side listener for FireClient/FireAllClients
OnServerInvoke              server-side function assigned for InvokeServer
```

---

## Common Mistakes

| Mistake | Fix |
|---|---|
| `OnServerEvent` in a LocalScript | Use `OnClientEvent` on client; `OnServerEvent` is server-only |
| Remotes in ServerStorage | Move to ReplicatedStorage |
| Trusting payload beyond player identity | Validate every field in the payload |
| Server→client RemoteFunction | Use RemoteEvent; frozen client stalls server thread |
| No `WaitForChild` in LocalScript | Remotes may not exist yet; always use `WaitForChild` |
| Multiple `OnServerInvoke` assignments | Only the last assignment wins; keep it in one place |
| Firing inside tight loop without throttle | Use `UnreliableRemoteEvent` or accumulate delta time |

