
Roblox Networking
- 52 installs
- 11 repo stars
- Updated August 3, 2026
- nonlooped/roblox-suite
Write server-authoritative Roblox client-server code that resists the hostile client using RemoteEvents, replication, network ownership, validation, and rate limiting.
About
Covers server-authoritative Roblox networking including RemoteEvent/RemoteFunction vs Bindables, replication, network ownership, validation, rate limiting, and exploit vectors. A developer uses it to place code on the correct side of the boundary and harden datastore, UI, animation, or monetization logic.
- RemoteEvent/RemoteFunction vs BindableEvent/BindableFunction placement
- Validation and rate limiting against exploit vectors
Roblox Networking by the numbers
- 52 all-time installs (skills.sh)
- +15 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #160 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nonlooped/roblox-suite --skill roblox-networkingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 52 |
|---|---|
| repo stars | ★ 11 |
| Last updated | August 3, 2026 |
| Repository | nonlooped/roblox-suite ↗ |
What it does
Write server-authoritative Roblox client-server code that resists the hostile client using RemoteEvents, replication, network ownership, validation, and rate limiting.
Files
roblox-networking
Core sources: https://create.roblox.com/docs/projects/client-server , https://create.roblox.com/docs/scripting/security/security-tactics (server-authority techniques), the entire scripting/security/ section (security-tactics, defensive-design, client-server-boundary, network-ownership, access-control, etc.), events/remote and bindable, plus the relevant Engine classes (RemoteEvent, RemoteFunction, Bindable*, RunService, etc.).
The Model in One Sentence
The server is authoritative for game state and simulation. The client simulates for responsiveness and renders what the server tells it. Anything that can be abused (economy, progression, combat outcomes, movement in competitive play) must be validated or simulated on the server.
Communication Tools
RemoteEvent — fire-and-forget across the boundary.
- Client → Server:
RemoteEvent:FireServer(...)→ server handler receives(player, ...) - Server → Client:
FireClient(player, ...)orFireAllClients(...)
RemoteFunction — request/response (yields).
- Client:
InvokeServer(...)→ serverOnServerInvoke(player, ...) - Server can InvokeClient (less common, has caveats).
Critical warning: RemoteFunction:InvokeClient yields the server until the targeted client returns a value. A hostile or lagging client can leave the invocation pending and hang the server indefinitely. Avoid server→client invocation; if you must use it, enforce a timeout and treat the client as untrusted.BindableEvent / BindableFunction — same-side only (server modules talking to each other, or client modules). Never cross the network boundary. Perfect for decoupling within one side.
General rule: Client sends intent. Server decides outcome and replicates the result (or lets normal Instance replication handle it).
Server Authority Techniques
- Validate every client action (distance, rate, prerequisites, stamina, line-of-sight, etc.).
- Simulate critical systems on the server (or at least re-execute the important parts).
- Use server tables or Attributes as source of truth, not leaderstats or client-visible values.
- For movement/physics in competitive games: rely on Roblox physics + network ownership + server validation/correction.
- Replicate only what each client needs (don't broadcast entire inventories every frame).
See the official server-authority techniques page for movement validation, physics, etc.
Network Ownership
Unanchored parts and assemblies have a network owner (usually the nearest player or the server, based on proximity and client capacity). The owner simulates the physics locally for low latency. Server can call BasePart:SetNetworkOwner(player or nil) to set ownership for the entire connected assembly. Anchored parts are always owned by the server and cannot be manually reassigned.
Critical for vehicles, projectiles, pushable objects, etc. Misuse leads to rubber-banding for other players or easy exploitation.
Common Exploit Classes & Defenses
- Speed/fly/noclip — server movement validation or physics ownership + correction.
- Remote spam / duplication — per-player rate limiting + server-side cooldowns + argument validation.
- LocalScript injection / free-model backdoors — audit assets, use capabilities, server validation.
- Tampering with leaderstats or client-visible values — treat them as pure display; authoritative state lives in server tables or DataStores.
- Information disclosure — don't parent secret models in replicated locations; use streaming, server-only storage (ServerStorage/ServerScriptService), and avoid putting sensitive geometry/triggers where clients can see them.
CanQueryand collision groups are not confidentiality controls; they only affect spatial queries and physics collisions, not replication or rendering.
Rate limiting pattern (simple but effective): Keep a table of last action times per player per action type. On Remote, check delta. Warn or kick on abuse.
Capabilities (experimental security layer): Script capabilities let you restrict what scripts are allowed to do at a granular level. They are currently experimental. To use them, set Workspace.SandboxedInstanceMode = Enum.SandboxedInstanceMode.Experimental, then set Sandboxed = true and assign a SecurityCapabilities value on the script or a parent container (Model/Folder). This restricts the script to only the abilities in its capabilities set.
Access Control & Confidentiality
- What the client can see: control with Instance streaming, don't parent sensitive geometry in Workspace if clients shouldn't know the exact layout, use server-only containers where possible.
- Confidential data (exact economy formulas, drop rates, anti-cheat parameters) should stay on the server.
Secure Monetization & Data Flows (tie-in to other skills)
- Prompt purchase on client.
- For game passes: only grant benefits inside
PromptGamePassPurchaseFinishedwhenwasPurchased == true, and re-verify ownership withUserOwnsGamePassAsynconPlayerAdded. - For developer products and subscriptions: validate and grant benefits only inside the server-side
ProcessReceiptcallback. - DataStore writes only from server Scripts.
- Any client Remote that says "I bought X, give me the item" must be ignored or treated as a hint and re-validated on the server.
Practical Placement Rules
- Authoritative logic, economy, progression, combat resolution, datastore access → ServerScriptService or server-only modules.
- UI, input handling, cosmetic prediction, local VFX → client (LocalScripts or client modules under PlayerGui or required from ReplicatedStorage with IsClient branches).
- Shared pure functions / constants / Remote definitions → ReplicatedStorage modules (loaded on both sides but executed in context).
Use RunService context checks everywhere you are unsure.
Scripts
scripts/RateLimiter.lua— a per-player, per-action token-bucket rate limiter with abuse escalation for sensitive Remotes. UseRateLimiter.new()for a dedicated instance orRateLimiter.defaultfor the shared singleton.
This skill + roblox-core + the domain skills (datastores, UI, animation, etc.) produces code that is both correct in behavior and resistant to the hostile client environment that is Roblox multiplayer.
Common Exploits and Defenses
Speed / Fly / Noclip
Defense:
- Server-side movement validation or position correction.
- Use proper network ownership + physics simulation on server for competitive games.
- Don't trust client-reported position for anything important.
Remote Spam / Duplication
Defense:
- Per-player, per-action rate limiting (timestamp table + threshold).
- Server-side cooldowns that the client cannot bypass.
- Argument sanitization on every Remote (type checks, range checks, instance validity).
Information Disclosure
Defense:
- Don't parent secret geometry, triggers, or logic in replicated locations.
- Use StreamingEnabled + streaming targets.
- Server-only storage (ServerStorage) for sensitive models.
CanQuery = falseand collision groups are not confidentiality controls. They only affect spatial query operations and physics collisions, not whether the client can see or replicate a part. Use them for physics/query filtering only.
LocalScript Injection / Free Model Backdoors
Defense:
- Never use untrusted free models in production.
- Audit all third-party code.
- Use Script Capabilities (experimental). Enable
Workspace.SandboxedInstanceMode = Enum.SandboxedInstanceMode.Experimental, then setSandboxed = trueand assign aSecurityCapabilitiesvalue on scripts or parent containers to restrict what they can do. - Server validation means even if client code is compromised, the server still decides outcomes.
Tampering with Client-Visible Values
Leaderstats, Attributes the client can see, etc. are display only.
Always keep authoritative values in server-side tables or DataStores. Update the visible values from the server result.
General Hardening
- Assume the client is hostile.
- Every Remote that can affect shared state must have validation.
- Log suspicious patterns (very high action rates, impossible values).
- Have a plan for consequence (kick, temporary restrictions, ban via
Players:BanAsync). Note:BanAsyncis server-only, yields, and requiresPlayers.BanningEnabledto be set before the server starts (enable it in Studio and publish, or set it very early in a server Script). The config must includeUserIds,Duration,DisplayReason, andPrivateReason, and optionallyApplyToUniverse. Wrap it inpcall.
The combination of good architecture (this skill) + proper data patterns (roblox-datastores skill) + secure monetization flows makes the majority of common exploits ineffective or easily detectable.
Remote and Bindable Patterns
RemoteEvent (Fire-and-Forget)
Best for most gameplay communication.
Client to Server (input/intent):
-- Client
RemoteEvent:FireServer("attack", targetPosition)
-- Server
RemoteEvent.OnServerEvent:Connect(function(player, action, data)
if action == "attack" then
-- validate player can attack, range, cooldown, etc.
-- then apply effect and replicate result if needed
end
end)Server to Client:
FireClient(player, ...)for one playerFireAllClients(...)for everyone- To target a specific subset of players, iterate over the desired players and call
FireClienton each; there are no built-in variants for specific player lists.
RemoteFunction (Request-Response)
Use sparingly because it yields the caller.
Good for:
- Client asking server for specific data that must be fresh (e.g. current shop prices, complex calculated stats)
- Server asking a specific client something (rarer)
Bad for frequent or performance-critical traffic.
Critical warning: RemoteFunction:InvokeClient yields the server until the targeted client returns a value. A hostile or lagging client can leave the invocation pending and hang the server indefinitely. Avoid server→client invocation; if you must use it, enforce a timeout and treat the client as untrusted.On the server, wrap OnServerInvoke handlers in pcall so an error in your validation logic does not propagate to the invoking client (which could leak internals or cause unexpected client-side failures).
BindableEvent / BindableFunction
Same side only.
- Server modules talking to other server modules
- Client modules talking to other client modules
Never put a Bindable in a place where it could be used to bypass the network boundary.
Excellent for clean event-driven architecture within one side of the client/server divide.
Recommended Patterns
1. Central Remotes folder in ReplicatedStorage. All RemoteEvents and RemoteFunctions live here with clear names. 2. Wrapper modules on both sides that expose clean APIs instead of raw Fire/OnEvent calls everywhere. 3. Validation layer on the server side of every Remote. 4. Rate limiting on sensitive or abusable Remotes. 5. Source of truth lives on the server. Remotes are for synchronization, not authority.
Argument Sanitization Examples
Validate every argument from the client before using it.
-- Type and bounds checks
local function sanitizeDamage(amount: unknown): number
if typeof(amount) ~= "number" then return 0 end
if amount ~= amount then return 0 end -- NaN check
return math.clamp(amount, 0, 100)
end
-- Instance validity and ancestry
local function sanitizeTarget(target: unknown, validFolder: Folder): Model?
if typeof(target) ~= "Instance" or not target:IsA("Model") then return nil end
if not target:IsDescendantOf(validFolder) then return nil end
return target
end
-- Whitelisted table keys
local ALLOWED_KEYS = { Name = true, Slot = true }
local function sanitizeOptions(options: unknown): { [string]: unknown }
if typeof(options) ~= "table" then return {} end
local result = {}
for key, value in pairs(options :: { [string]: unknown }) do
if ALLOWED_KEYS[key] and typeof(key) == "string" then
result[key] = value
end
end
return result
endWarning: A client-to-server Instance reference can point to any Instance the client can see, including other players' characters or replicated map geometry. The server must re-check the Instance's ClassName, ancestry, and whether the player is allowed to interact with it. Do not trust the client to send the "right" object.
Payload Sizes
Keep Remote payloads small. Sending large tables, long strings, or many Instances every frame can degrade server performance and increase bandwidth for all clients. Prefer compact identifiers (IDs, positions) and fetch detailed data on demand.
See the authority-and-validation reference for more validation and rate-limiting examples.
Server Authority and Validation
Core Principle
If it can give a player an advantage, the server must be the one that ultimately decides whether it happens.
Client can:
- Send "I want to do X"
- Predict the result for smooth visuals
- Display the outcome after server confirmation
Server must:
- Receive the intent
- Run all the important checks (distance, timing, cost, prerequisites, anti-cheat, etc.)
- Apply the change (or reject it)
- Replicate the authoritative result
Practical Validation Checklist (per remote)
- Is the player alive / in a valid state?
- Is the action on cooldown?
- Does the player have the required resources / level / item?
- Is the target in range / line of sight?
- Is the data the client sent reasonable (numbers within bounds, instances exist and are valid)?
- Has this player been rate-limited on this action recently?
Only after all checks pass do you apply the effect and save/persist as needed.
Common Implementation
Many teams keep a "Validator" or "ActionHandler" module on the server that all Remotes funnel through.
Example structure:
-- Server
local function handleAction(player, actionName, payload)
if not RateLimiter:canPerform(player, actionName) then return end
local validator = Validators[actionName]
if not validator or not validator(player, payload) then return end
-- Apply effect
Effects[actionName]\(player, payload\)
-- Replicate or let normal replication handle it
endConcrete Argument Sanitization
Sanitize every value the client sends before trusting it.
local function sanitizeNumber(value: unknown, min: number, max: number): number?
if typeof(value) ~= "number" then return nil end
if value ~= value then return nil end -- reject NaN
return math.clamp(value, min, max)
end
local function sanitizeInstance(value: unknown, expectedClass: string, ancestor: Instance): Instance?
if typeof(value) ~= "Instance" then return nil end
if not value:IsA(expectedClass) then return nil end
if not value:IsDescendantOf(ancestor) then return nil end
return value
end
local VALID_KEYS = { Slot = true, Amount = true }
local function sanitizeDictionary(value: unknown): { [string]: unknown }
if typeof(value) ~= "table" then return {} end
local out = {}
for k, v in pairs(value :: { [string]: unknown }) do
if typeof(k) == "string" and VALID_KEYS[k] then
out[k] = v
end
end
return out
endInstance references from the client can refer to any replicated Instance. Always re-validate the type, ancestry, and whether the player is permitted to interact with that specific object.
Network Ownership
For physics objects (vehicles, projectiles, pushable crates):
- The network owner simulates the physics.
- Server can change ownership with
SetNetworkOwner, which sets ownership for the entire connected assembly. - Anchored parts are always server-owned;
SetNetworkOwnercannot override them. - Automatic ownership is based on character proximity and client capacity, not simply "nearest player."
- Use
Workspace.SetNetworkOwnerAuto = falseto disable automatic ownership assignment entirely when you need full manual control (e.g., competitive or tightly-controlled physics). - Always validate important outcomes on the server regardless of who owns the physics.
When Client Prediction is Acceptable
- Cosmetic animations
- Local camera work
- UI state
- Short-term movement prediction (with server correction)
Never predict economy, unlocks, damage application, or quest progress.
See the exploits-and-defenses reference for how to handle the cases where clients try to lie.
--!strict
--[[
RateLimiter.lua
Per-player, per-action rate limiter using a token-bucket algorithm with abuse escalation.
Usage on server:
local RateLimiter = require(...)
local limiter = RateLimiter.new() -- dedicated instance
-- or
local limiter = RateLimiter.default -- shared singleton
if not limiter:canPerform(player, "BuyItem", 5) then
-- too fast
return
end
]]
local Players = game:GetService("Players")
local DEFAULT_CAPACITY = 10
local DEFAULT_REFILL_RATE = 1
local DEFAULT_MIN_INTERVAL = 0.5
local MAX_ACTIONS_PER_PLAYER = 64
local MAX_ABUSE_SCORE = 10
type AbuseRecord = {
tokens: number,
lastRefill: number,
abuseScore: number,
}
export type RateLimiter = {
records: { [number]: { [string]: AbuseRecord } },
_connection: RBXScriptConnection?,
new: () -> RateLimiter,
default: RateLimiter,
canPerform: (self: RateLimiter, player: Player, action: string, minInterval: number?) -> boolean,
clearPlayer: (self: RateLimiter, player: Player) -> (),
}
local RateLimiter = {}
RateLimiter.__index = RateLimiter
function RateLimiter.new(): RateLimiter
local self = setmetatable({}, RateLimiter) :: RateLimiter
self.records = {}
self._connection = Players.PlayerRemoving:Connect(function(player)
self:clearPlayer(player)
end)
return self
end
RateLimiter.default = RateLimiter.new()
function RateLimiter:canPerform(player: Player, action: string, minInterval: number?): boolean
if typeof(player) ~= "Instance" or not player:IsA("Player") then
error("RateLimiter.canPerform: player must be a Player instance", 2)
end
if typeof(action) ~= "string" then
error("RateLimiter.canPerform: action must be a string", 2)
end
local interval = minInterval or DEFAULT_MIN_INTERVAL
if typeof(interval) ~= "number" or interval ~= interval or interval <= 0 then
error("RateLimiter.canPerform: minInterval must be a positive number", 2)
end
local userId = player.UserId
local playerRecords = self.records[userId]
if not playerRecords then
playerRecords = {}
self.records[userId] = playerRecords
end
local actionCount = 0
for _ in pairs(playerRecords) do
actionCount += 1
end
if actionCount >= MAX_ACTIONS_PER_PLAYER and playerRecords[action] == nil then
return false
end
local now = os.clock()
local record = playerRecords[action]
if not record then
record = { tokens = DEFAULT_CAPACITY, lastRefill = now, abuseScore = 0 }
playerRecords[action] = record
end
local elapsed = now - record.lastRefill
record.tokens = math.min(DEFAULT_CAPACITY, record.tokens + elapsed * DEFAULT_REFILL_RATE)
record.lastRefill = now
local effectiveInterval = interval * (1 + record.abuseScore)
if record.tokens < 1 then
record.abuseScore = math.min(record.abuseScore + 0.5, MAX_ABUSE_SCORE)
return false
end
if now - (record.lastRefill - elapsed) < effectiveInterval then
record.abuseScore = math.min(record.abuseScore + 0.25, MAX_ABUSE_SCORE)
else
record.abuseScore = math.max(record.abuseScore - 0.1, 0)
end
record.tokens -= 1
return true
end
function RateLimiter:clearPlayer(player: Player): ()
if typeof(player) ~= "Instance" or not player:IsA("Player") then
error("RateLimiter.clearPlayer: player must be a Player instance", 2)
end
self.records[player.UserId] = nil
end
return RateLimiter