Remote Events and Callbacks

All experiences inherently communicate between the server and the players' clients. For example, as a player maneuvers their character on their client, certain Humanoid properties, such as states, are communicated to the server, which passes this information to the other connected clients.

RemoteEvent and RemoteFunction objects allow you to create your own events and callback functions to communicate custom behavior across the client-server boundary, with RemoteEvents facilitating one-way communication and RemoteFunctions facilitating two-way communication (sending a request across the boundary and yielding until a response is received from the recipient).

Quick Reference

The following tables serve as a quick reference for how to use RemoteEvents and RemoteFunctions to communicate between the client and server.

Client → Server
ClientRemoteEvent:FireServer(args)
ServerRemoteEvent.OnServerEvent:Connect(function(player, args))
Server → Client
ServerRemoteEvent:FireClient(player, args)
ClientRemoteEvent.OnClientEvent:Connect(function(args))
Server → All Clients
ServerRemoteEvent:FireAllClients(args)
ClientRemoteEvent.OnClientEvent:Connect(function(args))

Remote Events

A RemoteEvent object facilitates asynchronous, one-way communication across the client-server boundary without yielding for a response.

To create a new RemoteEvent via the Explorer window in Studio:

  1. Hover over the container into which you want to insert the RemoteEvent. In order to ensure both server and client access, it must be in a place where both sides can see it, such as ReplicatedStorage, although in some cases it's appropriate to store it in Workspace or inside a Tool.
  2. Click the button that appears to the right of the container's name and insert a RemoteEvent instance.
  3. Rename the instance to describe its purpose.

Once you've created a RemoteEvent, it can facilitate one-way communication from client to server, from server to client, or from the server to all clients.

Client → Server
Server → Client
Server → All Clients

Client → Server

You can use a LocalScript to trigger an event on the server by calling the FireServer() method on a RemoteEvent. If you pass arguments to FireServer(), they pass to the event handler on the server with certain limitations. Note that the first parameter of the event handler on the server is always the Player object of the client that calls it, and additional parameters follow.

ClientRemoteEvent:FireServer(args)
ServerRemoteEvent.OnServerEvent:Connect(function(player, args))

The following Script connects an event handler to OnServerEvent that creates a new Part on the server. The accompanying LocalScript then calls FireServer() on the RemoteEvent instance with the desired Color and Position for the part.

Event Connection - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get reference to remote event instance
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function onCreatePart(player, partColor, partPosition)
print(player.Name .. " fired the RemoteEvent")
local newPart = Instance.new("Part")
newPart.Color = partColor
newPart.Position = partPosition
newPart.Parent = workspace
end
-- Connect function to event
remoteEvent.OnServerEvent:Connect(onCreatePart)
Event Firing - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get reference to remote event instance
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
-- Fire the remote event and pass additional arguments
remoteEvent:FireServer(Color3.fromRGB(255, 0, 0), Vector3.new(0, 25, -20))

Server → Client

You can use a Script to trigger an event on a client by calling the FireClient() method on a RemoteEvent. The first argument for FireClient() is the Player object of the client that you want to respond to the event, and additional arguments pass to the client with certain limitations. Note that the event handler doesn't need to include the Player object as its first argument because you can determine the player on the client with Players.LocalPlayer.

ServerRemoteEvent:FireClient(player, args)
ClientRemoteEvent.OnClientEvent:Connect(function(args))

The following LocalScript connects an event handler to the OnClientEvent event. The accompanying Script then listens for incoming players to the server and calls FireClient() for each with arbitrary data.

Event Connection - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- Get reference to remote event instance
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local player = Players.LocalPlayer
local function onNotifyPlayer(maxPlayers, respawnTime)
print("[Client] Event received by player", player.Name)
print(maxPlayers, respawnTime)
end
-- Connect function to event
remoteEvent.OnClientEvent:Connect(onNotifyPlayer)
Event Firing - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- Get reference to remote event instance
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
-- Listen for incoming players and dispatch remote event to each
local function onPlayerAdded(player)
print("[Server] Firing event to player", player.Name)
remoteEvent:FireClient(player, Players.MaxPlayers, Players.RespawnTime)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Server → All Clients

You can use a Script to trigger an event on all clients by calling the FireAllClients() method on a RemoteEvent. Unlike FireClient(), the FireAllClients() method doesn't require a Player object because it fires the RemoteEvent to all clients.

ServerRemoteEvent:FireAllClients(args)
ClientRemoteEvent.OnClientEvent:Connect(function(args))

The following LocalScript connects an event handler to the OnClientEvent event which outputs a remaining countdown time. The accompanying Script then calls FireAllClients() in a loop every second to fire the RemoteEvent for all clients.

Event Connection - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get reference to remote event instance
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function onTimerUpdate(seconds)
print(seconds)
end
-- Connect function to event
remoteEvent.OnClientEvent:Connect(onTimerUpdate)
Event Firing - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get reference to remote event instance
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local countdown = 5
-- Fire the RemoteEvent every second until time expires
for timeRemaining = -1, countdown do
remoteEvent:FireAllClients(countdown - timeRemaining)
task.wait(1)
end

Remote Callbacks

A RemoteFunction object facilitates synchronous, two-way communication across the client-server boundary. The sender of a remote function will yield until it receives a response from the recipient.

To create a new RemoteFunction via the Explorer window in Studio:

  1. Hover over the container into which you want to insert the RemoteFunction. In order to ensure both server and client access, it must be in a place where both sides can see it, such as ReplicatedStorage, although in some cases it's appropriate to store it in Workspace or inside a Tool.
  2. Click the button that appears to the right of the container's name and insert a RemoteFunction instance.
  3. Rename the instance to describe its purpose.

Once you've created a RemoteFunction, it can facilitate two-way communication between client and server or between server and client.

Client → Server → Client
Server → Client → Server

Client → Server → Client

You can use a LocalScript to call a function on the server by calling the InvokeServer() method on a RemoteFunction. Unlike a remote event, the LocalScript that invokes the RemoteFunction yields until the callback returns. Arguments that you pass to InvokeServer() pass to the OnServerInvoke callback of the RemoteFunction with certain limitations. Note that if you define multiple callbacks to the same RemoteFunction, only the last definition executes.

ClientRemoteFunction:InvokeServer(args)
ServerRemoteFunction.OnServerInvoke = function(player, args)

The following Script defines the callback function via OnServerInvoke and returns the requested Part through its return value. The accompanying LocalScript then calls InvokeServer() with extra arguments defining the requested part color and position.

Callback Connection - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get reference to remote function instance
local remoteFunction = ReplicatedStorage:FindFirstChildOfClass("RemoteFunction")
-- Callback function
local function createPart(player, partColor, partPosition)
print(player.Name .. " requested a new part")
local newPart = Instance.new("Part")
newPart.Color = partColor
newPart.Position = partPosition
newPart.Parent = workspace
return newPart
end
-- Set function as remote function's callback
remoteFunction.OnServerInvoke = createPart
Event Invocation - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get reference to remote function instance
local remoteFunction = ReplicatedStorage:FindFirstChildOfClass("RemoteFunction")
-- Pass a color and position when invoking the callback
local newPart = remoteFunction:InvokeServer(Color3.fromRGB(255, 0, 0), Vector3.new(0, 25, -20))
-- Output the returned part reference
print("The server created the requested part:", newPart)

Server → Client → Server

You can use a Script to call a function on the client by calling the InvokeClient() method on a RemoteFunction, but it has serious risks as follows:

For actions that don't require two-way communications, such as updating a GUI, use a RemoteEvent and communicate from server to client.

Argument Limitations

When you fire a RemoteEvent or invoke a RemoteFunction, it forwards any arguments that you pass with the event or to the callback function. Any type of Roblox object such as an Enum, Instance, or others can be passed, as well as Luau types such as numbers, strings, and booleans, although you should carefully explore the following limitations.

Non-String Indices

If any indices of a passed table are non-string types such as an Instance, userdata, or function, Roblox automatically converts those indices to strings.

Event Connection - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function onEventFire(passedTable)
for k, v in passedTable do
print(typeof(k)) --> string
end
end
-- Connect function to event
remoteEvent.OnClientEvent:Connect(onEventFire)
Event Firing - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
-- Listen for incoming players and dispatch remote event to each
local function onPlayerAdded(player)
remoteEvent:FireClient(player,
{
[workspace.Baseplate] = true
}
)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Passed Functions

Functions included as arguments for a RemoteEvent or RemoteFunction will not be replicated across the client-server boundary, making it impossible to pass functions remotely. Instead, the resulting argument on the receiving side will be nil.

Event Connection - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function onClientEvent(func)
print(func) --> nil
end
remoteEvent.OnClientEvent:Connect(onClientEvent)
Event Firing - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function testFunction()
print("Hello world!")
end
-- Fire remote event with function as an argument
remoteEvent:FireAllClients(testFunction)

Table Indexing

If you pass a table of data, do not pass a mixed table of numeric and string keys. Instead, pass a table that consists entirely of key-value pairs (dictionary) or entirely of numeric indices.

Event Connection - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function onEventFire(player, passedTable)
for k, v in passedTable do
print(k .. " = " .. v)
--> 1 = Sword
--> 2 = Bow
--> CharName = Diva Dragonslayer
--> CharClass = Rogue
end
end
-- Connect function to event
remoteEvent.OnServerEvent:Connect(onEventFire)
Event Firing - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
-- Numerically indexed table
local inventoryData = {
"Sword", "Bow"
}
-- Dictionary table
local characterData = {
CharName = "Diva Dragonslayer",
CharClass = "Rogue"
}
remoteEvent:FireServer(inventoryData)
remoteEvent:FireServer(characterData)

Table Identities

Tables passed as arguments to remote events/callbacks are copied, meaning they will not be exactly equivalent to those provided when firing the event or invoking the callback. Nor will tables returned to the invoker be exactly equivalent to those provided. You can demonstrate this by running the following script on a RemoteFunction and observing how the table identities differ.

Callback Connection - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteFunction = ReplicatedStorage:FindFirstChildOfClass("RemoteFunction")
-- Callback function
local function returnTable(player, passedTable)
-- Output table identity on invocation
print(tostring(passedTable)) --> table: 0x48eb7aead27563d9
return passedTable
end
-- Set function as remote function's callback
remoteFunction.OnServerInvoke = returnTable
Event Invocation - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteFunction = ReplicatedStorage:FindFirstChildOfClass("RemoteFunction")
local inventoryData = {
"Sword", "Bow"
}
-- Output original table identity
print(tostring(inventoryData)) --> table: 0x059bcdbb2b576549
local invokeReturn = remoteFunction:InvokeServer(inventoryData)
-- Output table identity upon return
print(tostring(invokeReturn)) --> table: 0x9fcae7919563a0e9

Metatables

If a table has a metatable, all of the metatable information is lost in the transfer. In the following code sample, the NumWheels property is part of the Car metatable. When the server receives the following table, the truck table has the Name property but not the NumWheels property.

Event Connection - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local function onEvent(player, param)
print(param) --> {["Name"] = "MyTruck"}
end
-- Connect function to event
remoteEvent.OnServerEvent:Connect(onEvent)
Event Firing - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
local Car = {}
Car.NumWheels = 4
Car.__index = Car
local truck = {}
truck.Name = "MyTruck"
setmetatable(truck, Car)
-- Fire event with table including a metatable
remoteEvent:FireServer(truck)

Non-Replicated Instances

If a RemoteEvent or RemoteFunction passes a value that's only visible to the sender, Roblox doesn't replicate it across the client-server boundary and passes nil instead of the value. For example, if a Script passes a descendant of ServerStorage, the client listening to the event will receive a nil value because that object isn't replicable for the client.

Event Firing - Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
-- Will be received as "nil" because client can't access ServerStorage
local storedPart = Instance.new("Part")
storedPart.Parent = ServerStorage
local function onPlayerAdded(player)
remoteEvent:FireClient(player, storedPart)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Similarly, if you create a part in a LocalScript and try to pass it to a Script, the server will see nil because the part isn't replicable for the server.

Event Firing - LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteEvent = ReplicatedStorage:FindFirstChildOfClass("RemoteEvent")
-- Will be received as "nil" because the server doesn't know about this part
local clientPart = Instance.new("Part")
clientPart.Parent = workspace
remoteEvent:FireServer(clientPart)