Skip to content

Networking overview

How ChloeKernel moves data between client and server: five channel kinds over one RemoteEvent, a fail-closed inbound pipeline, and where each module lives.

All reliable channel traffic between client and server rides one RemoteEvent carrying schema-packed buffers. Unreliable streams ride their own UnreliableRemoteEvents. On top of that wire sits a channel layer built around one decision: inbound traffic is rejected unless something explicitly accepts it. A client payload reaches a handler only after it passes the transport limits, a session gate, a per-channel token bucket and a fail-closed validation chain. A channel with no validator rejects every payload until one is registered or the channel is marked Open = true.

The client renders and asks. It sends intents (“use item 3”), never state (“my coins are now 9999”). The server validates every intent and is the only thing that changes state. Client code is always readable by exploiters, so the defense is never trusting it.

A game declares its channels in one module, ReplicatedStorage.Game.Channels, and the Channels guide walks through that workflow.

Client Server
Registry.client(Channels) Registry.server(kernel, Channels)
fire / invoke / on / predict on / handle / send / broadcast
| |
v v
NetClient NetDriver
typed handles, no validation session gate, token bucket,
fail-closed hook chain, handler
| |
v v
Packet <----------------- one RemoteEvent -----------------> Packet
schema-packed buffers batched per heartbeat, U8 channel ids,
varint frame lengths server to client, byte and message budgets on the server

Each layer has one job:

  • Packet is the wire. It packs arguments into buffers by typed schema ("NumberU16" costs exactly 2 bytes), batches every channel into one flush per heartbeat, and multiplexes them over one RemoteEvent with U8 channel ids. On the server it enforces per-player byte and message budgets before anything decodes.
  • NetDriver on the server and NetClient on the client are the channel layer: named, typed channels with a direction, and on the server the gate every inbound message passes.
  • The Registry is the definition layer. One shared module declares every channel for both sides, so schemas cannot drift, and its Validate rules compile into the validation chain.

Around them sit Prediction for optimistic client actions, Replica for replicated state, NetGovernor for per-client link grading, BusBridge for Bus topics across the boundary and MessageDriver for events between servers.

Code placement is attack surface. Everything that decides runs on the server; the client holds only what it executes.

Piece Module Replicates to clients
NetDriver (gates, rate limits, audits) ServerScriptService.ChloeKernelServer.Net Never
ReplicaService, NetGovernor, BusBridge, MessageDriver ServerScriptService.ChloeKernelServer.* Never
NetClient, ReplicaClient, GovernorClient, BusBridgeClient ReplicatedStorage.ChloeKernel.Net.* Yes
Registry ReplicatedStorage.ChloeKernel.Net.Registry Yes. Definitions are shared by design
Packet and TransportGuard ReplicatedStorage.ChloeKernel.Net.Packet Yes. The limits are checked in server memory
TokenBucket ReplicatedStorage.ChloeKernel.Net.TokenBucket Yes. The buckets themselves are created and checked in server memory
Prediction ReplicatedStorage.ChloeKernel.Net.Prediction Yes. Rollback is a client concern
Game channel definitions ReplicatedStorage.Game.Channels Yes

An exploiter can read the TokenBucket module and the Registry rules. The instances that enforce them live in server state and cannot be changed from a client.

Five kinds, each a different answer to who talks and what happens on failure:

Kind Direction Delivery Gate Use it for
Intent client to server reliable full inbound pipeline Player actions. Fire and forget
PredictedIntent client to server, ack back reliable full inbound pipeline; the ack follows the handler Actions the client shows at once and rolls back on rejection
Request client to server to client reliable, yields full inbound pipeline; every failure answers RejectValue Questions with answers (shop stock, eligibility)
State server to client reliable none; the server is trusted One-shot events (kill feed, toasts, round events)
Unreliable server to client unreliable, latest wins per stream, at most 980 bytes none High-frequency cosmetic data where a lost update beats a late one
Goal Use Notes
Send a player action to the server Intent The default. See NetDriver
Make an ability feel instant and keep the server authoritative PredictedIntent The client applies the effect at once; the server acks or it rolls back. See Prediction
Ask the server a question and wait for the answer Request Yields. Rejection and timeout both resume with RejectValue
Tell clients something happened State One-shot events. Values that evolve belong in a replica
Show clients a value that changes over time A replica Joiners get a snapshot; changes go out as field deltas
Stream 20-60 Hz cosmetic data that tolerates loss Unreliable Serde structs over UnreliableRemoteEvent. Stale datagrams drop unless Ordered = false
Mirror Bus topics across the network BusBridge Server topics broadcast down; client topics come up as whitelisted intents
Send events to other servers MessageDriver Uses MessagingService; carries no player traffic

Every client-to-server message walks these stages in order. Failing any stage stops it; the handler is the last thing to run.

# Stage Lives in On failure
1 Byte budget: 8,000 bytes per player per heartbeat window, each call counted as at least 800 Packet The whole payload drops before any decode. Reported as ByteLimit
2 Call shape: the payload is a buffer and the instances argument a table or nil Packet The payload drops after paying its 800 bytes. Reported as Malformed
3 Message budget: one token per frame, 240 per second, burst 128 Packet The rest of the payload drops before its next frame decodes. Reported as MessageLimit
4 Frame decode by the channel’s schema, with Instance slots checked and typed arrays capped at 8,192 elements per message Packet A decode error drops the rest of the payload. A duplicate of a pending request is skipped and reported as DuplicateRequest
5 Session gate: the sender has an active session whose profile is not locked by a transaction NetDriver Dropped. Runs before bucket creation, so a message deferred past PlayerRemoving cannot key a bucket to a departed player
6 Token bucket: RateLimit per second and Burst capacity, per player per channel NetDriver Dropped; Net.RateLimited hook and Bus topic. Predicted intents get no ack and roll back on timeout
7 Fail-closed absence check: no validator and no Open = true NetDriver Everything rejects; one warning names the channel
8 Registry Validate rules (Range, OneOf, MaxLength) at priority 5 Hooks Rejected before game code runs
9 Game validators: Hooks:on("Intent.<name>", fn) in priority order (default 100) Hooks false rejects, and so does a crashing validator. Validators may rewrite Args, and the handler receives the result
10 Recheck: the session is still active, still the player’s, and still unlocked after validators that yielded NetDriver Rejected
11 Handler NetDriver Runs under pcall. An error counts as a rejection and never reaches the client

Stages 1 to 4 are covered on the Packet page and reported through Net.FloodDetected. For Requests, every failure from stage 5 on answers the channel’s RejectValue, never an error message or a hint about which gate fired. A request dropped at stages 1 to 4 never reaches the driver; the client waits out its timeout and resumes with RejectValue all the same.

Fail-closed usually means a crashing validator rejects. ChloeKernel extends it to absence: a missing validator is not permission.

  • A crashing validator rejects, unless the hook point was set FailOpen, and securityAudit reports such a gate as a High finding.
  • An Intent or Request with no validator registered rejects every payload, whether a handler or only an Intent.<name> Bus subscriber consumes it. This closes the unguarded GrantCoins intent whose handler trusts a client-supplied amount, and since 0.9.0 the same channel consumed only through the Bus.
  • A channel that needs no authorization (a payload-less ready-up, an idempotent toggle) says so with Open = true, a searchable, auditable declaration.
  • Registry Validate rules count as a validator.
  • Rejections are silent to the sender: intents do not happen, requests answer RejectValue, predicted intents ack a rollback. A probing client learns nothing about which gate stopped it.

Net:auditValidators() lists every channel still missing both, and Net:securityAudit() sweeps the whole surface (unguarded channels, Open channels, fail-open gates, defaulted rate limits). Details are on the NetDriver page; the fuzzing side is Fuzz and audit.

One intent, defined once, gated by a declarative rule and one game validator. The Game layer injects the handles as self.Net. List the service in src/Server/Game/Registry.luau and the controller in src/Client/Game/Registry.luau (see Services and controllers).

src/Shared/Game/Channels.luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Registry = require(ReplicatedStorage.ChloeKernel.Net.Registry)
return Registry.define({
UseItem = { Kind = "Intent", Schema = { "NumberU16" }, RateLimit = 5, Validate = { [1] = { Range = { 1, 999 } } } },
})
src/Server/Game/Services/ItemService.luau
--!strict
-- ItemService: applies item uses after a per-player cooldown.
local ItemService = {
Name = "ItemService",
Dependencies = {},
}
local CooldownSeconds = 0.5
function ItemService.init(self: any, kernel: any)
self.Kernel = kernel
-- Game validator: server-side state, never a client claim
kernel.Hooks:on("Intent.UseItem", function(context)
if os.clock() - (context.Session.Data.LastItemUse or 0) < CooldownSeconds then
return false
end
return nil
end)
self.Net.UseItem.on(function(session, itemId: number)
session.Data.LastItemUse = os.clock()
print(`{session.Player.Name} used item {itemId}`)
end)
end
function ItemService.start(_self: any) end
function ItemService.stop(_self: any) end
return ItemService

A rejected intent costs the client nothing and the server one pass through the chain, so validating everything has no performance excuse.

The driver counts everything it does. kernel:stats().Net exposes IntentsAccepted, IntentsRejected, IntentsRateLimited, RequestsRejected, TransportFloods and TransportFloodBytes. The Net.RateLimited and Net.FloodDetected hook points and Bus topics feed the anti-exploit layer (see Rate limiting). The debug panel’s NET window shows decoded messages on both sides of the wire with their verdict: OK, Rejected, RateLimited, Unguarded, NoSession, TransactionLocked, Error, Dropped or Shed. Capture runs only while the window is open on a client, and on the server only while an allowlisted debugger is connected.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.