Client-server communication - RemoteEvent, UnreliableRemoteEvent, RemoteFunction, what survives serialization, typed remote modules, server validation and rate limiting, bandwidth (deltas, buffers, batching), synced time, Zap/Blink. Use when adding remotes, syncing state to clients, debugging nil/lost remote arguments, or reducing lag and bandwidth.
Installs into .claude/skills of the current project.
Are you the author of Roblox Networking?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/el4cteo-roblox-networking)
---
name: roblox-networking
description: Client-server communication - RemoteEvent, UnreliableRemoteEvent, RemoteFunction, what survives serialization, typed remote modules, server validation and rate limiting, bandwidth (deltas, buffers, batching), synced time, Zap/Blink. Use when adding remotes, syncing state to clients, debugging nil/lost remote arguments, or reducing lag and bandwidth.
---
# Roblox networking
## Pick the right channel
| Channel | Delivery | Use for |
| --- | --- | --- |
| `RemoteEvent` | Reliable, ordered | Requests (client→server), game events, UI updates. The default. |
| `UnreliableRemoteEvent` | May drop or reorder; payload ≤ **1000 bytes** (larger payloads are dropped) | Continuous, superseded-by-next data: aim direction, cosmetic VFX, positions of non-physics objects. |
| `RemoteFunction` | Request/response, yields caller | Client→server queries that need an answer (e.g. "can I buy X?"). |
| Attributes / properties | Automatic replication server→client | Persistent state clients read (health, team, round state). |
| Physics replication | Automatic for owned assemblies | Moving parts; see `roblox-physics`. |
Never use `RemoteFunction:InvokeClient`: if the client errors, disconnects, or never returns, the server
throws or **yields forever**. Use a `RemoteEvent` to the client and another back if you need a reply.
## Defining remotes once (typed)
Create remotes on the server; clients wait for them. Centralize names so typos can't happen.
```luau
--!strict
-- ReplicatedStorage/Shared/Net.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local FOLDER_NAME = "Remotes"
local IS_SERVER = RunService:IsServer()
local folder: Instance
if IS_SERVER then
folder = ReplicatedStorage:FindFirstChild(FOLDER_NAME) or Instance.new("Folder")
folder.Name = FOLDER_NAME
folder.Parent = ReplicatedStorage
else
folder = ReplicatedStorage:WaitForChild(FOLDER_NAME)
end
local function get<T>(className: string, name: string): T
if IS_SERVER then
local existing = folder:FindFirstChild(name)
if existing then
return existing :: any
end
local remote = Instance.new(className)
remote.Name = name
remote.Parent = folder
return remote :: any
end
return folder:WaitForChild(name) :: any
end
return {
RequestPurchase = get("RemoteEvent", "RequestPurchase") :: RemoteEvent,
InventoryChanged = get("RemoteEvent", "InventoryChanged") :: RemoteEvent,
AimDirection = get("UnreliableRemoteEvent", "AimDirection") :: UnreliableRemoteEvent,
GetShopStock = get("RemoteFunction", "GetShopStock") :: RemoteFunction,
}
```
## Server handler template
Every `OnServerEvent`/`OnServerInvoke` handler: **rate limit → type-check every argument → check
game rules → act**. The first parameter is always the real `Player` (Roblox fills it in; it can't be forged).
```luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Net = require(ReplicatedStorage.Shared.Net)
local ITEM_PRICES: { [string]: number } = { Sword = 100, Shield = 150 }
local MIN_INTERVAL = 0.25
local lastRequest: { [Player]: number } = {}
Net.RequestPurchase.OnServerEvent:Connect(function(player: Player, itemId: unknown)
local now = os.clock()
if now - (lastRequest[player] or 0) < MIN_INTERVAL then
return -- rate limited
end
lastRequest[player] = now
if typeof(itemId) ~= "string" or #itemId > 64 then
return -- malformed: never trust argument types
end
local price = ITEM_PRICES[itemId]
if not price then
return -- unknown item
end
-- check the player's server-side balance, deduct, grant, then notify:
Net.InventoryChanged:FireClient(player, { added = itemId })
end)
game:GetService("Players").PlayerRemoving:Connect(function(player)
lastRequest[player] = nil -- avoid leaking Player keys
end)
```
Full validation toolkit (NaN/inf checks, instance ownership, distance and cooldown checks): `roblox-security`.
## What survives the wire
- Tables are **copied**: identity and metatables are lost; functions become `nil`.
- Don't mix array and dictionary keys in one table; don't put `nil` holes in arrays.
- Non-string dictionary keys (Instances, numbers in sparse tables) are converted to strings — send arrays of pairs instead.
- Instances arrive as `nil` if the receiver can't see them (e.g. `ServerStorage` items, or parts not
yet streamed in on the client).
- Supported: `nil`, booleans, numbers, strings, tables, `buffer`, Roblox datatypes (`Vector3`, `CFrame`,
`Color3`, `EnumItem`, ...), and replicated `Instance` references.
- Server→client ordering vs. property replication is not guaranteed: a remote may arrive before or
after a property change it relates to.
## Bandwidth and CPU
- Send **changes**, not full state (item added, not the whole inventory). Send on change, not per frame.
- Throttle input-driven sends on the client (e.g. aim at most 20 Hz) and still rate-limit on the server.
- Send IDs/enums as small numbers; quantize floats; pack hot, high-frequency data into a `buffer`.
- Batch many small messages into one per frame (queue in a table, flush on `Heartbeat`).
- Server-side `TweenService` replicates every frame and looks jittery: tween on clients instead.
- VFX: the server decides the outcome and fires a minimal event; each client spawns effects locally.
- Profile with the MicroProfiler (`ProcessPackets`, `Allocate Bandwidth and Run Senders`) and the
Developer Console **Network** tab.
```luau
--!strict
-- Pack a hit notification into 11 bytes instead of a table with string keys.
local function encodeHit(targetId: number, damage: number, position: Vector3): buffer
local b = buffer.create(11)
buffer.writeu32(b, 0, targetId)
buffer.writeu16(b, 4, math.clamp(math.round(damage), 0, 65535))
buffer.writei16(b, 6, math.clamp(math.round(position.X), -32768, 32767))
buffer.writei16(b, 8, math.clamp(math.round(position.Z), -32768, 32767))
buffer.writeu8(b, 10, math.clamp(math.round(position.Y / 4), 0, 255))
return b
end
local function decodeHit(b: buffer): (number, number, Vector3)
return buffer.readu32(b, 0),
buffer.readu16(b, 4),
Vector3.new(buffer.readi16(b, 6), buffer.readu8(b, 10) * 4, buffer.readi16(b, 8))
end
print(decodeHit(encodeHit(42, 25, Vector3.new(10, 20, 30))))
```
For many remotes with hot paths, schema-driven generators (**Zap**, **Blink**) produce typed,
validated, buffer-packed remotes automatically. Adopt them when profiling shows network cost.
## Time and latency
- `workspace:GetServerTimeNow()` is a synchronized clock on server and clients: send a start timestamp
and let clients compute progress locally (round timers, cooldown bars, projectiles).
- `player:GetNetworkPing()` returns round-trip network latency in seconds (clients may only query
`LocalPlayer`); use it to size lag-compensation windows.
- Don't send countdown ticks every second; send the end time once.
More patterns (request/response with timeouts, state snapshots + deltas, per-frame batching, client
prediction): [references/patterns.md](references/patterns.md).
## Related skills
`roblox-security` (validation, anti-exploit), `roblox-architecture` (where remotes live),
`roblox-physics` (network ownership, server authority), `roblox-performance`.