Skip to content
Engine

Players and Networking

NetworkService

Named remotes with built-in validation, per-player rate limiting, request/response Promises, batching, streams, middleware and bandwidth stats.

ServiceShared

Getting it

local NetworkService = Engine:GetService("NetworkService")

Overview

NetworkService replaces hand-made RemoteEvents and RemoteFunctions with remotes defined in code. A remote has a name, a direction and optional rules, and the service creates and wires the underlying instances for you. Every message from a client is checked before your code sees it:

  1. Middleware you registered with Use can drop or rewrite it.
  2. The Schema (a Guard schema) must accept the arguments.
  3. The rate limit for that player must allow it (on by default: 20 calls per second, bursts of 40).

Anything that fails fires NetworkService.Violation on the server instead of reaching your handler.

Define each remote on both sides with the same name and settings, typically in a shared ModuleScript. The server creates the remote; the client waits for it (up to DefaultConnectionTimeout seconds).

Shared module: ReplicatedStorage.Remotes
local Engine = require(game:GetService("ReplicatedStorage"):WaitForChild("Engine"))
-- On the server, require ServerScriptService.Engine instead; see "Server and Client".
local NetworkService = Engine:GetService("NetworkService")
local Guard = Engine:GetPackage("Guard")

return {
	Equip = NetworkService:DefineRemote("Equip", {
		Direction = "ClientToServer",
		Schema = Guard.tuple(Guard.string({ MaxLength = 32 })),
	}),
	GetInventory = NetworkService:DefineRemote("GetInventory", {
		Direction = "ClientToServer",
		Mode = "Request",
	}),
	Hit = NetworkService:DefineRemote("Hit", {
		Direction = "ServerToClient",
		Batch = { Rate = 30 },
	}),
}
Server
Remotes.Equip.Received:Connect(function(player, args)
	equip(player, args[1])
end)

Remotes.GetInventory:SetHandler(function(player)
	return getInventory(player)
end)

Remotes.Hit:Fire(player, damage, position) -- to one player
Remotes.Hit:Fire(nil, damage, position)    -- to everyone
Client
Remotes.Equip:Fire("Sword")

Remotes.GetInventory:RequestAsync():andThen(function(items)
	renderInventory(items)
end)

Remotes.Hit.Received:Connect(function(_, args)
	showDamageNumber(args[1], args[2])
end)

Remotes

DefineRemote

NetworkService:DefineRemote(name: string, def: RemoteDef): Remote

Defines a remote. Each name can be defined once per side. Remotes can also be defined in configuration (Remotes = { [name] = def }), which creates them when the service loads.

FieldDescription
Direction"ClientToServer", "ServerToClient" or "Both". Required. Sending the wrong way raises an error.
Mode"Fire" (default): one-way messages. "Request": the sender gets a Promise with the other side's reply.
SchemaA Guard schema checked against the arguments. The arguments arrive as an array, so Guard.tuple(...) is the natural fit. Checked when sending too, so mistakes show up early.
Wire + MaxBytesA Serde type used to measure the arguments; messages larger than MaxBytes are rejected. MaxBytes needs Wire.
RateLimit{ RatePerSec, Burst }, any RateLimiter limiter, or false to turn it off. Applies per player to messages the server receives.
Batch{ Rate?, Ports?, Reliable? }. Sends Fire messages in packets a fixed number of times per second instead of one remote call each. Only for one-way Fire remotes with a single direction.
Reliablefalse uses an UnreliableRemoteEvent: faster, but messages may be lost. Good for frequent position updates.
Channel, CategoryAddress slots 1-255 for organising connections; see Addresses.

Remote

MemberDescription
:Fire(...)Client: sends to the server. Server: :Fire(player, ...) sends to one player, :Fire(nil, ...) to everyone.
:RequestAsync(...): PromiseRequest remotes only. Resolves with the handler's return values, or rejects on error, validation failure, missing handler or after DefaultRequestTimeout seconds. The server passes the target player first.
:SetHandler(fn)Request remotes only, on the receiving side. Called as fn(player, ...args) on the server (player is nil on the client); its return values are the reply.
Received: Signal<(player, args)>Fire remotes: fires for each valid incoming message. args is a packed array (args.n holds the count). player is nil on the client.
:Destroy()Removes the remote and rejects pending requests.
Name, Id, Mode, DirectionRead-only.

GetRemote

NetworkService:GetRemote(name: string): Remote?

Validation and safety

Use

NetworkService:Use(fn: (ctx: { Player: Player?, RemoteName: string, Args: { any } }) -> (boolean, { any }?))

Adds middleware that runs on every incoming remote message, in the order added. Return false to drop the message, or true plus a new argument list to rewrite it.

NetworkService:Use(function(ctx)
	if ctx.Player and isBanned(ctx.Player) then
		return false
	end
	return true
end)

Violation

NetworkService.Violation: Signal<(player: Player?, remoteName: string, reason: string)>

Fires when an incoming message fails validation or the rate limit. Pair it with ModerationSystem to flag exploiters.

RateLimited

NetworkService.RateLimited: Signal<(player: Player?, remoteName: string)>

Streams

A stream sends many small messages in packets at a fixed rate, with optional double sending (Ports = 2) so a lost packet does not lose data. Batched remotes use streams internally; you can also create one directly.

CreateStream

NetworkService:CreateStream(settings: StreamSettings): Stream -- server

GetStream

NetworkService:GetStream(id: string, settings: StreamSettings): Stream -- client

Connects to a stream the server created, by its Id (send the id to the client, for example in an attribute).

SettingDescription
Direction"ClientToServer" or "ServerToClient".
RatePackets per second; snapped to the nearest of AllowedPacketRates.
Ports1, or 2 to send every packet twice.
ReliableReliable or unreliable delivery.
MaxEntriesPerPacket, MaxQueueSizeLimits. Entries beyond the queue size are dropped and Overflowed fires.
Channel, CategoryAddress slots.
Stream memberDescription
:Send(...)Queues a message. The server passes a player first, or nil for everyone.
Received: Signal<(player, args)>Fires per received message.
Overflowed: SignalFires when the queue is full.
:GetQueueSize(), :Destroy()
Id, Direction, Rate, Ports, ReliableRead-only.

Raw messages

For quick prototypes NetworkService also has plain, unnamed messaging: FireServer, FireClient, FireAllClients, their FireUnreliable... variants, InvokeServer, InvokeClient, the OnServerInvoke / OnClientInvoke properties, and the RawReceived: Signal<(player, args)> event.

Important

Raw messages have no validation and no rate limit. As soon as any remote is defined, StrictMode turns on and raw client-to-server calls raise an error, so a forgotten raw path cannot be abused. Engine's own services define remotes when they load, so expect StrictMode to be on in most games. Set StrictModeLocksServerToClient to also lock raw server-to-client calls.

Addresses

Every connection gets an address Game.Channel.Category.Connection, used as its Id. Channel 0 belongs to Engine's own services; your connections use channels 1 to 255 (default from configuration). Channels and categories let you group connections, for example one channel per game mode. GameNumber is the first part: set it in configuration, or leave it at 0 to derive a fixed number from game.GameId.

Stats

GetStats

NetworkService:GetStats(): { InKbps: number, OutKbps: number, PerRemote: { { Name, Kbps, CallsPerSec } } }

Bandwidth over the last second, in total and per remote. Sizes are estimates.

Properties

PropertyDescription
StrictMode: booleanRead-only. true once any remote is defined.
GameNumber: numberRead-only. The game part of every address.
OnServerInvoke, OnClientInvokeHandlers for raw invokes.

Configuration

Defaults from DefaultConfiguration/Services/NetworkService16 settings
SettingDefaultWhat it does
GameNumber0First number (1-255) of every connection address (Game.Channel.Category.Connection); 0 or anything outside 1-255 derives a fixed one from game.GameId instead.
DefaultChannel1Channel (1-255) a connection gets when none is given; channel 0 is Engine's own and can't be picked.
DefaultCategory1Category (1-255) a connection gets when none is given.
AllowedPacketRates{ 4, 8, 12, 24, 30, 60, 120 }Packet rates a Stream may flush at; a requested Rate snaps to the nearest.
DefaultPacketRate30Packets per second a Stream flushes at when settings.Rate is omitted.
DefaultPorts1Underlying RemoteEvents (1 or 2) a Stream/Batch uses when Ports is omitted; 2 sends every packet twice for loss resilience.
DefaultReliabletrueWhether a Stream/Remote uses reliable delivery when Reliable is omitted.
DefaultMaxEntriesPerPacket200Entries a Stream packs into one packet when settings.MaxEntriesPerPacket is omitted.
DefaultMaxQueueSize1000Hard cap on queued-but-unsent entries per outbox.
DefaultDedupWindow256Recent Seq numbers remembered per sender for 2-port dedup.
DefaultConnectionTimeout10Seconds a client waits for a server-created connection/stream before erroring.
DefaultRequestTimeout10Seconds before Remote:RequestAsync()'s Promise times out.
DefaultRateLimitPerSec20Client-to-server calls per second a Remote allows per player when it defines no limiter.
DefaultRateLimitBurst40Burst a Remote's default per-player rate limiter allows.
StrictModeLocksServerToClientfalseWhether StrictMode (active once any Remote is defined) also locks out raw server-to-client calls.
Remotes{}{ [name] = RemoteDef } defined at boot, same as an imperative :DefineRemote call.

Types

export type Remote
export type RemoteDef
export type RemoteMode = "Fire" | "Request"
export type RemoteDirection = "ClientToServer" | "ServerToClient" | "Both"
export type Stream
export type StreamSettings
export type Address = { Game: number, Channel: number, Category: number, Connection: number }
export type Stats