I had been curious about this before and had run into conflicting information that tended to be anecdotal rather than empirical, so I did some testing. I did client-server and server-client traffic using 100 calls to a single remote and 1 call each to 100 remotes.
The code, if interested.
Client Code:
--!strict
--//CLIENT TEST//
task.wait(10)
local replicated_storage = game:GetService("ReplicatedStorage")
local ping_remote = replicated_storage:WaitForChild("PingEvent") --For communication outside of the test space.
local single_remote = replicated_storage:WaitForChild("SingleRemote") --For single-remote tests.
local remotes: {RemoteEvent} =
replicated_storage:WaitForChild("Remotes100"):GetChildren() :: any --For multi-remote tests.
local ping_start: number
local function process_request(count: number): ()
if count == 1 then
ping_start = os.clock()
elseif count == 100 then
local stop = os.clock()
print("TOTAL PROCESSING TIME: " .. (stop - ping_start) * 1000000 .. " microseconds.")
end
end
single_remote.OnClientEvent:Connect(process_request)
for _, remote in ipairs(remotes) do
remote.OnClientEvent:Connect(process_request)
end
print("PINGING SERVER.")
ping_remote:FireServer()
ping_remote.OnClientEvent:Wait()
print("RECEIVED BATON FROM SERVER")
task.wait(2)
print("CLIENT-SERVER SINGLE REMOTE")
for count = 1, 100 do
single_remote:FireServer(count)
end
task.wait(2)
print("CLIENT-SERVER MULTIPLE REMOTES")
for index, remote in ipairs(remotes) do
remote:FireServer(index)
end
Server Code:
--!strict
--//SERVER TEST//
local replicated_storage = game:GetService("ReplicatedStorage")
local ping_remote = replicated_storage:WaitForChild("PingEvent") --For communication outside of the test space.
local single_remote = replicated_storage:WaitForChild("SingleRemote") --For single-remote tests.
local remotes: {RemoteEvent} =
replicated_storage:WaitForChild("Remotes100"):GetChildren() :: any --For multi-remote tests.
local ping_start: number
local function process_request(_: Player, count: number): ()
if count == 1 then
ping_start = os.clock()
elseif count == 100 then
local stop = os.clock()
print("TOTAL PROCESSING TIME: " .. (stop - ping_start) * 1000000 .. " microseconds.")
end
end
single_remote.OnServerEvent:Connect(process_request)
for _, remote in ipairs(remotes) do
remote.OnServerEvent:Connect(process_request)
end
ping_remote.OnServerEvent:Connect(function(player: Player): ()
print("RECEIVED INITIALIZATION PING FROM PLAYER: " .. player.Name)
task.wait(2)
print("SERVER-CLIENT SINGLE REMOTE")
for count = 1, 100 do
single_remote:FireClient(player, count)
end
task.wait(2)
print("SERVER-CLIENT MULTIPLE REMOTES")
for index, remote in ipairs(remotes) do
remote:FireClient(player, index)
end
task.wait(2)
print("PASSING BATON TO CLIENT")
ping_remote:FireClient(player)
end)
Each request was just a number from 1 to 100, in order. On receiving the “1” packet, the time would be marked, and upon receiving the “100” packet, the duration would be determined. Here is the average receivers’ processing duration of each type of request in microseconds:
|
Server → Client |
|
|
Client → Server |
|
|
Single |
Multi |
|
Single |
Multi |
Studio |
142 μs |
229 μs |
|
242 μs |
379 μs |
Live |
149 μs |
226 μs |
|
236 μs |
515 μs |
Note that fluctuating latency will have an impact on live server testing, so expect some outliers.
From these tests, it seems that fewer remotes perform better than more remotes, but this test is done in a vacuum with simple requests. Your mileage may vary depending on system complexity and whether or not using multiple remotes could gain you performance benefits in event processing (for instance, a multi-event approach may be able to take advantage of upvalues in the scope of the event handler whereas a single-remote structure is more likely to require table lookups to find everything it needs, although I’m not sure how much that would realistically net you).
In the end, though, you’re most likely too see gains in better management of the requests themselves than in the number of remotes used to deliver those requests.