Players and Networking
NetworkService
Named remotes with built-in validation, per-player rate limiting, request/response Promises, batching, streams, middleware and bandwidth stats.
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:
- Middleware you registered with
Usecan drop or rewrite it. - The Schema (a Guard schema) must accept the arguments.
- 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).
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 },
}),
}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 everyoneRemotes.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): RemoteDefines 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.
| Field | Description |
|---|---|
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. |
Schema | A 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 + MaxBytes | A 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. |
Reliable | false uses an UnreliableRemoteEvent: faster, but messages may be lost. Good for frequent position updates. |
Channel, Category | Address slots 1-255 for organising connections; see Addresses. |
Remote
| Member | Description |
|---|---|
:Fire(...) | Client: sends to the server. Server: :Fire(player, ...) sends to one player, :Fire(nil, ...) to everyone. |
:RequestAsync(...): Promise | Request 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, Direction | Read-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 -- serverGetStream
NetworkService:GetStream(id: string, settings: StreamSettings): Stream -- clientConnects to a stream the server created, by its Id (send the id to the client, for example in an attribute).
| Setting | Description |
|---|---|
Direction | "ClientToServer" or "ServerToClient". |
Rate | Packets per second; snapped to the nearest of AllowedPacketRates. |
Ports | 1, or 2 to send every packet twice. |
Reliable | Reliable or unreliable delivery. |
MaxEntriesPerPacket, MaxQueueSize | Limits. Entries beyond the queue size are dropped and Overflowed fires. |
Channel, Category | Address slots. |
| Stream member | Description |
|---|---|
:Send(...) | Queues a message. The server passes a player first, or nil for everyone. |
Received: Signal<(player, args)> | Fires per received message. |
Overflowed: Signal | Fires when the queue is full. |
:GetQueueSize(), :Destroy() | |
Id, Direction, Rate, Ports, Reliable | Read-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
| Property | Description |
|---|---|
StrictMode: boolean | Read-only. true once any remote is defined. |
GameNumber: number | Read-only. The game part of every address. |
OnServerInvoke, OnClientInvoke | Handlers for raw invokes. |
Configuration
DefaultConfiguration/Services/NetworkService16 settings| Setting | Default | What it does |
|---|---|---|
| GameNumber | 0 | First 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. |
| DefaultChannel | 1 | Channel (1-255) a connection gets when none is given; channel 0 is Engine's own and can't be picked. |
| DefaultCategory | 1 | Category (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. |
| DefaultPacketRate | 30 | Packets per second a Stream flushes at when settings.Rate is omitted. |
| DefaultPorts | 1 | Underlying RemoteEvents (1 or 2) a Stream/Batch uses when Ports is omitted; 2 sends every packet twice for loss resilience. |
| DefaultReliable | true | Whether a Stream/Remote uses reliable delivery when Reliable is omitted. |
| DefaultMaxEntriesPerPacket | 200 | Entries a Stream packs into one packet when settings.MaxEntriesPerPacket is omitted. |
| DefaultMaxQueueSize | 1000 | Hard cap on queued-but-unsent entries per outbox. |
| DefaultDedupWindow | 256 | Recent Seq numbers remembered per sender for 2-port dedup. |
| DefaultConnectionTimeout | 10 | Seconds a client waits for a server-created connection/stream before erroring. |
| DefaultRequestTimeout | 10 | Seconds before Remote:RequestAsync()'s Promise times out. |
| DefaultRateLimitPerSec | 20 | Client-to-server calls per second a Remote allows per player when it defines no limiter. |
| DefaultRateLimitBurst | 40 | Burst a Remote's default per-player rate limiter allows. |
| StrictModeLocksServerToClient | false | Whether 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