ワンクリックで
roblox-networking
Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
DataStores, ProfileStore, session locking, data persistence patterns.
Animations, particles, tweens, ContentProvider, visual effects.
GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
Luau language fundamentals, type system, OOP, deprecation table, error patterns.
ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction.
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
| name | roblox-networking |
| description | Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening. |
| last_reviewed | "2026-05-22T00:00:00.000Z" |
Load this reference when:
This document covers server-side validation, rate limiting, suspicion scoring, and server-authoritative design patterns. For player lifecycle (PlayerAdded/Removing), see roblox-architecture.
Every RemoteEvent payload is attacker-controlled. Validate type, range, ownership, and cooldown on the server for every request.
StarterPlayerScripts, StarterGui, ReplicatedStorage, or ReplicatedFirst is fully readable.The client is a display layer, not a trusted authority. It renders the world and collects input. The server decides what actually happens.
A useful mental model: treat every RemoteEvent:FireServer() call as if it were an HTTP request from an anonymous stranger on the internet. Validate everything. Assume nothing.
For runtime type checking, the
tlibrary is vendored atvendor/t/t.lua(osyrisrblx/t v3.1.1, MIT). It provides composable type checks (t.string,t.number,t.interface({...})) that are cleaner than manual typeof() chains. The agent can place it when relevant.
A bare remote handler like this is exploitable:
-- BAD: No validation at all
DamageRemote.OnServerEvent:Connect(function(player, targetName, damage)
local target = Players:FindFirstChild(targetName)
target.Character.Humanoid:TakeDamage(damage)
end)
An exploiter can fire this with any target name and any damage value, instantly killing anyone.
Place this in ServerScriptService:
-- ServerScriptService/Modules/RemoteValidator.luau
local RemoteValidator = {}
--[[ -----------------------------------------------------------------------
Type Checking
Validates that arguments match expected types.
----------------------------------------------------------------------- ]]
type TypeSpec = string | (value: any) -> boolean
function RemoteValidator.checkType(value: any, expected: TypeSpec): boolean
if typeof(expected) == "function" then
return expected(value)
end
return typeof(value) == expected
end
function RemoteValidator.validateArgs(
args: { any },
schema: { { name: string, type: TypeSpec, optional: boolean? } }
): (boolean, string?)
for i, spec in schema do
local value = args[i]
if value == nil then
if not spec.optional then
return false, `Missing required argument: {spec.name}`
end
continue
end
if not RemoteValidator.checkType(value, spec.type) then
return false, `Invalid type for {spec.name}: expected {tostring(spec.type)}, got {typeof(value)}`
end
end
-- Reject extra arguments that were not declared in the schema
if #args > #schema then
return false, `Too many arguments: expected {#schema}, got {#args}`
end
return true, nil
end
--[[ -----------------------------------------------------------------------
Range Checking
Validates that numeric values fall within acceptable bounds.
----------------------------------------------------------------------- ]]
function RemoteValidator.checkRange(value: number, min: number, max: number): boolean
return typeof(value) == "number"
and value == value -- NaN check
and value >= min
and value <= max
end
function RemoteValidator.checkIntegerRange(value: number, min: number, max: number): boolean
return RemoteValidator.checkRange(value, min, max)
and math.floor(value) == value
end
--[[ -----------------------------------------------------------------------
Cooldown Tracking
Per-player, per-action cooldown enforcement.
----------------------------------------------------------------------- ]]
local cooldowns: { [Player]: { [string]: number } } = {}
function RemoteValidator.checkCooldown(player: Player, action: string, cooldownSeconds: number): boolean
local now = os.clock()
local playerCooldowns = cooldowns[player]
if not playerCooldowns then
playerCooldowns = {}
cooldowns[player] = playerCooldowns
end
local lastUsed = playerCooldowns[action]
if lastUsed and (now - lastUsed) < cooldownSeconds then
return false
end
playerCooldowns[action] = now
return true
end
function RemoteValidator.clearPlayerCooldowns(player: Player)
cooldowns[player] = nil
end
--[[ -----------------------------------------------------------------------
Existence Checks
Validates that targets, objects, and instances actually exist.
----------------------------------------------------------------------- ]]
function RemoteValidator.playerExists(playerName: string): Player?
local Players = game:GetService("Players")
return Players:FindFirstChild(playerName) :: Player?
end
function RemoteValidator.characterAlive(player: Player): boolean
local character = player.Character
if not character then
return false
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then
return false
end
return humanoid.Health > 0
end
function RemoteValidator.instanceExists(parent: Instance, name: string, className: string?): Instance?
local child = parent:FindFirstChild(name)
if not child then
return nil
end
if className and not child:IsA(className) then
return nil
end
return child
end
--[[ -----------------------------------------------------------------------
Authorization
Checks if a player is allowed to perform an action.
----------------------------------------------------------------------- ]]
function RemoteValidator.playerOwnsItem(player: Player, itemId: string, inventoryFolder: Folder?): boolean
local folder = inventoryFolder or player:FindFirstChild("Inventory") :: Folder?
if not folder then
return false
end
return folder:FindFirstChild(itemId) ~= nil
end
function RemoteValidator.playerHasAttribute(player: Player, attribute: string, expectedValue: any?): boolean
local value = player:GetAttribute(attribute)
if expectedValue ~= nil then
return value == expectedValue
end
return value ~= nil
end
--[[ -----------------------------------------------------------------------
Distance Check
Validates that two positions are within an acceptable range.
----------------------------------------------------------------------- ]]
function RemoteValidator.withinRange(posA: Vector3, posB: Vector3, maxDistance: number): boolean
return (posA - posB).Magnitude <= maxDistance
end
function RemoteValidator.playerWithinRange(player: Player, targetPos: Vector3, maxDistance: number): boolean
local character = player.Character
if not character then
return false
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return false
end
return RemoteValidator.withinRange(root.Position, targetPos, maxDistance)
end
--[[ -----------------------------------------------------------------------
Cleanup
----------------------------------------------------------------------- ]]
game:GetService("Players").PlayerRemoving:Connect(function(player)
RemoteValidator.clearPlayerCooldowns(player)
end)
return RemoteValidator
-- ServerScriptService/RemoteHandlers/DamageHandler.server.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Validator = require(ServerScriptService.Modules.RemoteValidator)
local DamageRemote = ReplicatedStorage.Remotes.DealDamage
local MAX_DAMAGE = 50
local DAMAGE_COOLDOWN = 0.5 -- seconds
local ATTACK_RANGE = 15 -- studs
local ARG_SCHEMA = {
{ name = "targetPlayer", type = "Instance" },
{ name = "damage", type = "number" },
}
DamageRemote.OnServerEvent:Connect(function(player: Player, ...: any)
local args = { ... }
-- 1. Validate argument types
local valid, err = Validator.validateArgs(args, ARG_SCHEMA)
if not valid then
warn(`[DamageHandler] {player.Name}: {err}`)
return
end
local targetPlayer: Player = args[1]
local damage: number = args[2]
-- 2. Validate the target is actually a Player
if not targetPlayer:IsA("Player") then
return
end
-- 3. Validate damage range
if not Validator.checkIntegerRange(damage, 1, MAX_DAMAGE) then
warn(`[DamageHandler] {player.Name}: damage out of range ({damage})`)
return
end
-- 4. Cooldown check
if not Validator.checkCooldown(player, "DealDamage", DAMAGE_COOLDOWN) then
return
end
-- 5. Verify attacker is alive
if not Validator.characterAlive(player) then
return
end
-- 6. Verify target is alive
if not Validator.characterAlive(targetPlayer) then
return
end
-- 7. Range check -- attacker must be near the target
local targetRoot = targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart")
if not targetRoot then
return
end
if not Validator.playerWithinRange(player, targetRoot.Position, ATTACK_RANGE) then
warn(`[DamageHandler] {player.Name}: target out of range`)
return
end
-- 8. Authorization -- verify the player has a weapon equipped
local character = player.Character
local weapon = character and character:FindFirstChildOfClass("Tool")
if not weapon or not weapon:GetAttribute("CanDealDamage") then
warn(`[DamageHandler] {player.Name}: no valid weapon equipped`)
return
end
-- 9. Server calculates actual damage (never trust client damage value directly)
local serverDamage = math.min(damage, weapon:GetAttribute("MaxDamage") or MAX_DAMAGE)
-- 10. Apply damage
local targetHumanoid = targetPlayer.Character:FindFirstChildOfClass("Humanoid")
if targetHumanoid then
targetHumanoid:TakeDamage(serverDamage)
end
end)
The server owns all game state. The client requests actions; the server decides outcomes.
-- ServerScriptService/Security/MovementValidator.server.luau
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local MAX_SPEED = 50 -- studs per second (walk + sprint + tolerance)
local MAX_VERTICAL_SPEED = 100 -- studs per second (jumping/falling tolerance)
local VIOLATION_THRESHOLD = 5 -- strikes before action
local CHECK_INTERVAL = 0.5 -- seconds between checks
local playerData: { [Player]: {
lastPosition: Vector3,
lastCheck: number,
violations: number,
} } = {}
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
local root = character:WaitForChild("HumanoidRootPart")
playerData[player] = {
lastPosition = root.Position,
lastCheck = os.clock(),
violations = 0,
}
end)
end)
Players.PlayerRemoving:Connect(function(player)
playerData[player] = nil
end)
RunService.Heartbeat:Connect(function()
local now = os.clock()
for player, data in playerData do
if (now - data.lastCheck) < CHECK_INTERVAL then
continue
end
local character = player.Character
if not character then
continue
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
continue
end
local dt = now - data.lastCheck
local displacement = root.Position - data.lastPosition
local horizontalSpeed = Vector3.new(displacement.X, 0, displacement.Z).Magnitude / dt
local verticalSpeed = math.abs(displacement.Y) / dt
if horizontalSpeed > MAX_SPEED or verticalSpeed > MAX_VERTICAL_SPEED then
data.violations += 1
warn(`[MovementValidator] {player.Name}: speed violation #{data.violations} (h={math.floor(horizontalSpeed)}, v={math.floor(verticalSpeed)})`)
if data.violations >= VIOLATION_THRESHOLD then
-- Teleport player back to last valid position
root.CFrame = CFrame.new(data.lastPosition)
-- Or kick for persistent abuse:
-- player:Kick("Movement anomaly detected.")
end
else
-- Decay violations over time for legitimate edge cases
data.violations = math.max(0, data.violations - 1)
data.lastPosition = root.Position
end
data.lastCheck = now
end
end)
-- Server decides damage, not the client.
local function calculateDamage(attacker: Player, weapon: Tool, target: Player): number?
local weaponConfig = WeaponDatabase[weapon.Name]
if not weaponConfig then
return nil
end
-- Server checks weapon cooldown
local lastFire = weapon:GetAttribute("LastFired") or 0
if os.clock() - lastFire < weaponConfig.Cooldown then
return nil
end
-- Server checks range
local attackerRoot = attacker.Character and attacker.Character:FindFirstChild("HumanoidRootPart")
local targetRoot = target.Character and target.Character:FindFirstChild("HumanoidRootPart")
if not attackerRoot or not targetRoot then
return nil
end
local distance = (attackerRoot.Position - targetRoot.Position).Magnitude
if distance > weaponConfig.Range then
return nil
end
-- Server calculates damage
weapon:SetAttribute("LastFired", os.clock())
return weaponConfig.BaseDamage
end
-- WRONG: Client tells server how much to add
CurrencyRemote.OnServerEvent:Connect(function(player, amount)
player.leaderstats.Gold.Value += amount -- exploiter sends 999999
end)
-- RIGHT: Server calculates the reward
QuestCompleteRemote.OnServerEvent:Connect(function(player, questId)
-- Validate quest ID type
if typeof(questId) ~= "string" then
return
end
-- Server checks quest state
local questData = PlayerQuestData[player]
if not questData or not questData[questId] then
return
end
if questData[questId].completed then
return -- already claimed
end
-- Server looks up the reward from its own data
local questConfig = QuestDatabase[questId]
if not questConfig then
return
end
-- Server awards the reward
questData[questId].completed = true
player.leaderstats.Gold.Value += questConfig.Reward
end)
-- Server-side trade validation
local function executeTrade(playerA: Player, playerB: Player, itemIdA: string, itemIdB: string): boolean
-- Both players must be alive and in range
if not Validator.characterAlive(playerA) or not Validator.characterAlive(playerB) then
return false
end
-- Verify ownership on the server
local invA = playerA:FindFirstChild("Inventory")
local invB = playerB:FindFirstChild("Inventory")
if not invA or not invB then
return false
end
---
## Rate Limiting
Roblox's built-in throttle (~500 req/sec per client) is NOT a substitute for custom rate limiting. Players can still spam remotes at hundreds of requests per second. You need application-level throttling.
### Pattern 1: Per-Player Cooldown Table
Simple and effective for most games. Each remote has a minimum time between calls per player.
```luau
local cooldowns: {[Player]: {[string]: number}} = {}
local COOLDOWN = 0.2 -- seconds between calls
local function isThrottled(player: Player, remoteName: string): boolean
local now = os.clock()
if not cooldowns[player] then
cooldowns[player] = {}
end
local lastCall = cooldowns[player][remoteName]
if lastCall and (now - lastCall) < COOLDOWN then
return true -- throttled
end
cooldowns[player][remoteName] = now
return false
end
-- Clean up when player leaves
Players.PlayerRemoving:Connect(function(player)
cooldowns[player] = nil
end)
-- Usage
BuyItem.OnServerEvent:Connect(function(player, itemId)
if isThrottled(player, "BuyItem") then return end
-- process purchase
end)
Define all remotes in one place with rate limits, validation, and allowed states. Cleaner than scattered OnServerEvent handlers.
type RemoteDef = {
RateLimit: number?,
Validate: (Player, ...any) -> boolean,
Handler: (Player, ...any) -> (),
}
local Remotes: {[string]: RemoteDef} = {
BuyItem = {
RateLimit = 0.5,
Validate = function(player, itemId)
return typeof(itemId) == "string" and #itemId < 50
end,
Handler = function(player, itemId)
-- process purchase
end,
},
EquipTool = {
RateLimit = 0.3,
Validate = function(player, toolId)
return typeof(toolId) == "string"
end,
Handler = function(player, toolId)
-- equip tool
end,
},
}
-- Wire up automatically
for name, def in Remotes do
local remote = ReplicatedStorage:WaitForChild(name)
remote.OnServerEvent:Connect(function(player, ...)
if def.RateLimit and isThrottled(player, name) then return end
if not def.Validate(player, ...) then return end
def.Handler(player, ...)
end)
end
For high-stakes games. Track suspicious behavior over time instead of hard-blocking.
local suspicion: {[Player]: number} = {}
local SUSPICION_THRESHOLD = 10
local DECAY_RATE = 1 -- points lost per second
local function addSuspicion(player: Player, amount: number, reason: string)
suspicion[player] = (suspicion[player] or 0) + amount
if suspicion[player] >= SUSPICION_THRESHOLD then
warn(`High suspicion for {player.Name}: {reason}`)
end
end
-- In remote handler
BuyItem.OnServerEvent:Connect(function(player, itemId)
if isThrottled(player, "BuyItem") then
addSuspicion(player, 2, "rate limit exceeded")
return
end
-- normal processing
end)
-- Decay suspicion over time
task.spawn(function()
while true do
task.wait(1)
for player, score in suspicion do
suspicion[player] = math.max(0, score - DECAY_RATE)
end
end
end)
-- BAD: no rate limiting at all
BuyItem.OnServerEvent:Connect(function(player, itemId)
-- exploiter can call this 1000 times/second
grantItem(player, itemId)
end)
-- BAD: client-side rate limiting (exploiter bypasses)
-- Rate limiting MUST be server-side
Source: Roblox Server-Side Detection Guide (Roblox/creator-docs, MIT), DevForum rate limiting patterns