A thin, typed wrapper around RemoteEvent, UnreliableRemoteEvent, and RemoteFunction for Roblox — one module for both server and client.
No middleware, no serialization layer, no magic. Just less boilerplate: instances are created lazily on the server, waited for on the client, cached by name, and every method delegates directly to the underlying signal, so there is zero per-fire overhead beyond a single method lookup.
- One API on both sides — require the same module from a
Scriptor aLocalScript; it detects the runtime and returns the right implementation. - Lazy creation — the server creates the
Remotesfolder and instances on first use. No manual setup inReplicatedStorage. - Cached by name — calling
Remote.Event("Hit")from ten different scripts returns the same object with no repeatedFindFirstChild/WaitForChildcost. - UnreliableRemoteEvent support — first-class support for high-frequency, loss-tolerant data (positions, aim, cosmetic effects).
- Class-safe names — requesting a name that already exists as a different remote class throws a clear error instead of silently returning the wrong instance.
- Extra fire helpers —
FireAllExceptandFireClientsfor common broadcast patterns. --!stricttyped — exported types for annotating your own code (Event,ServerEvent,ClientEvent,RemoteFn,ServerFunction,ClientFunction).
Roblox Studio: create a ModuleScript named Remote in ReplicatedStorage (or anywhere both sides can require it) and paste in Remote.luau.
Rojo: drop Remote.luau into your shared source folder, e.g.
src/
shared/
Remote.luau → ReplicatedStorage/Shared/Remote
Server
local Remote = require(ReplicatedStorage.Shared.Remote)
local DamageDealt = Remote.Event("DamageDealt")
DamageDealt:Connect(function(player, targetId, amount)
-- Always validate client input — arguments are untyped and untrusted
if typeof(targetId) ~= "number" or typeof(amount) ~= "number" then
return
end
-- ... apply damage on the server's terms
end)
DamageDealt:FireAllClients(targetId, actualDamage)Client
local Remote = require(ReplicatedStorage.Shared.Remote)
local DamageDealt = Remote.Event("DamageDealt")
DamageDealt:Connect(function(targetId, amount)
showDamageNumber(targetId, amount)
end)
DamageDealt:FireServer(targetId, amount)High-frequency data — use an unreliable event and throttle to a fixed tick:
local Movement = Remote.UnreliableEvent("Movement")
-- Fire at 10–20 Hz, never per frame; payloads must stay ≤ 900 bytesRequest/response
-- Server
local GetProfile = Remote.Function("GetProfile")
GetProfile.OnInvoke = function(player)
return profiles[player.UserId]
end
-- Client
local profile = Remote.Function("GetProfile"):Invoke()| Function | Returns | Backing instance |
|---|---|---|
Remote.Event(name) |
Event |
RemoteEvent |
Remote.UnreliableEvent(name) |
Event |
UnreliableRemoteEvent |
Remote.Function(name) |
RemoteFn |
RemoteFunction |
All constructors are idempotent: the first call creates (server) or waits for (client) the instance; later calls return the cached object. Names are shared across all three kinds — reusing a name with a different constructor throws.
| Method | Side | Description |
|---|---|---|
:Connect(callback) |
both | Connects to OnServerEvent (server, callback receives player first) or OnClientEvent (client). Returns RBXScriptConnection. |
:Once(callback) |
both | Like Connect, auto-disconnects after the first fire. |
:Wait() |
both | Yields until the next fire and returns its arguments. |
:FireServer(...) |
client | Fires to the server. |
:FireClient(player, ...) |
server | Fires to one player. |
:FireAllClients(...) |
server | Fires to everyone. |
:FireAllExcept(excluded, ...) |
server | Fires to everyone except one player. |
:FireClients(players, ...) |
server | Fires to a specific list of players. |
Calling a method on the wrong side (e.g. FireServer on the server) errors at runtime; the shared Event type intentionally exposes both sides so one module works everywhere. Annotate with ServerEvent/ClientEvent if you want the type checker to narrow it.
| Member | Side | Description |
|---|---|---|
.OnInvoke = fn |
both | Assigns the handler (OnServerInvoke / OnClientInvoke under the hood). Server handlers receive player as the first argument. |
:Invoke(...) |
client | Calls the server handler and yields for the result. |
:Invoke(player, ...) |
server | Calls InvokeClient — avoid this (see warnings). |
- Avoid server → client
Invoke.InvokeClientyields the server thread forever if the client never responds (disconnects, hangs, or an exploiter deliberately stalls). Prefer a pair of events for round-trips; if you must use it, wrap it intask.spawn+pcall. - Validate everything on the server. This wrapper passes arguments through untouched (
...any). Exploiters can fire any remote with any payload — always check types and ranges, and rate-limit sensitive handlers. - Don't fire per frame. Batch small frequent messages and flush on a fixed tick; use
UnreliableEventfor loss-tolerant streams. - Client requires can yield. On the client, constructors use
WaitForChild, so requiring an event the server never creates yields indefinitely (Roblox prints an "Infinite yield possible" warning after 5 seconds to help you spot it). - One name, one kind.
Event,UnreliableEvent, andFunctionshare a namespace; the module errors loudly on a class mismatch rather than handing you the wrong instance.
MIT — see LICENSE.