10 Remotes and validation
Before you start and what you will build
Continue the collectibles place. Remove the temporary boundary experiment from lesson 09. Keep Collectibles, CoinHud, and the obby scripts. You will add a free speed boost: collect at least three coins, approach a green station, and press a button. The server grants five seconds at WalkSpeed 28, with twenty seconds between accepted activations. Coins are an eligibility threshold and are not spent.
A RemoteEvent carries asynchronous messages. FireServer reaches OnServerEvent; Roblox supplies the sending Player as the callback's first argument. FireClient sends feedback to one player. The client must not provide a player identity to impersonate another user. See Remote events and callbacks.
Exact setup
Stop the test before editing. The obby no longer has a Baseplate, so first build a connected side platform rather than putting the station over a gap:
- Inside
Workspace.Obby, add an ordinary Part namedBoostPlatform. Set Size to12, 1, 12, Position to0, 1, 12, Anchored to true, and CanCollide to true. Its near edge at Z=6 meets Platform0's far edge at Z=6, and both surfaces are Y=1.5. - Add an ordinary Part directly under Workspace named
BoostStation. Set Size to6, 1, 6, Position to0, 2, 12, Anchored to true, CanCollide to true, and Color to green. It sits on BoostPlatform, clear of the Start checkpoint and the course's coins. - In Play, walk from Start toward positive Z onto BoostPlatform before collecting coins. Then collect three different coins and return across the same course platforms to Platform0 and the side platform. A reset uses your latest checkpoint, so return by traversing the course; do not assume resetting sends you to Start.
Distance is measured from the character root to BoostStation's center. Keep the exact positions for the first pass, then change the layout only after both approach and return routes work.
Add one Script BoostServer and one LocalScript BoostClient:
Workspace
Obby
BoostPlatform new anchored Part beside Platform0
BoostStation Part on BoostPlatform
ServerScriptService
Collectibles existing Script
BoostServer new Script
StarterPlayer
StarterPlayerScripts
CoinHud existing LocalScript
BoostClient new LocalScript
The server creates these during Play; do not create a second copy manually:
ReplicatedStorage
BoostRemotes Folder
RequestBoost RemoteEvent
BoostFeedback RemoteEvent
Complete server implementation
Paste this into BoostServer. There is no yielding between the final checks and updating the activation state, preventing a second handler from passing the same cooldown window while the first waits.
Download 10-boost-server.server.luau
-- Script: ServerScriptService > BoostServer
-- Lesson 11 replaces this script's contents; do not run both versions.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Config = {
RequiredCoins = 3,
Radius = 12,
Speed = 28,
Duration = 5,
Cooldown = 20,
RequestInterval = 0.5,
}
local station = workspace:WaitForChild("BoostStation", 10)
assert(station and station:IsA("BasePart"), "Create Workspace.BoostStation Part")
assert(not ReplicatedStorage:FindFirstChild("BoostRemotes"), "Run only one BoostServer")
local remotes = Instance.new("Folder")
remotes.Name = "BoostRemotes"
local request = Instance.new("RemoteEvent")
request.Name = "RequestBoost"
request.Parent = remotes
local feedback = Instance.new("RemoteEvent")
feedback.Name = "BoostFeedback"
feedback.Parent = remotes
remotes.Parent = ReplicatedStorage
local lastRequest = {}
local nextAllowed = {}
local active = {}
local function reply(player, requestId, ok, message)
feedback:FireClient(player, requestId, ok, message)
end
request.OnServerEvent:Connect(function(player, action, requestId)
local now = os.clock()
if now - (lastRequest[player] or -math.huge) < Config.RequestInterval then
return -- Do not multiply a flood by sending a reply for every rejected request.
end
lastRequest[player] = now
if typeof(requestId) ~= "number" or requestId % 1 ~= 0
or requestId < 1 or requestId > 1000000000 then
return -- Correlation metadata must also be bounded and well-formed.
end
if typeof(action) ~= "string" or action ~= "speed" then
reply(player, requestId, false, "Unknown request")
return
end
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = character and character:FindFirstChild("HumanoidRootPart")
if not humanoid or humanoid.Health <= 0 or not root or not root:IsA("BasePart") then
reply(player, requestId, false, "Respawn before requesting a boost")
return
end
local distance = (root.Position - station.Position).Magnitude
if not (distance <= Config.Radius) then -- Also rejects NaN distances.
reply(player, requestId, false, "Move closer to the green boost station")
return
end
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if not coins or not coins:IsA("IntValue") or coins.Value < Config.RequiredCoins then
reply(player, requestId, false, "Collect " .. Config.RequiredCoins .. " coins first")
return
end
local remaining = (nextAllowed[player] or 0) - now
if remaining > 0 or active[player] then
reply(player, requestId, false, "Boost cooling down; try again shortly")
return
end
-- No yielding between final validation and committing the state change.
nextAllowed[player] = now + Config.Cooldown
local token = {}
active[player] = token
local previousSpeed = humanoid.WalkSpeed
humanoid.WalkSpeed = Config.Speed
reply(player, requestId, true, "Boost active for " .. Config.Duration .. " seconds")
task.delay(Config.Duration, function()
if active[player] ~= token then return end
active[player] = nil
if player.Character == character and humanoid.Parent
and humanoid.WalkSpeed == Config.Speed then
humanoid.WalkSpeed = previousSpeed
end
if player.Parent == Players then
reply(player, requestId, true, "Boost ended; wait for the cooldown before reusing")
end
end)
end)
Players.PlayerRemoving:Connect(function(player)
lastRequest[player] = nil
nextAllowed[player] = nil
active[player] = nil
end)
Complete client implementation
Paste this into BoostClient. The generated button uses Activated so the same handler supports click and touch, plus gamepad activation when selected. Gamepad navigation and screen fit still require testing; the event alone does not create a good control layout. See GuiButton.
Download 10-boost-client.client.luau
-- LocalScript: StarterPlayer > StarterPlayerScripts > BoostClient
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local player = Players.LocalPlayer
local remotes = ReplicatedStorage:WaitForChild("BoostRemotes")
local request = remotes:WaitForChild("RequestBoost")
local feedback = remotes:WaitForChild("BoostFeedback")
local gui = Instance.new("ScreenGui")
gui.Name = "BoostGui"
gui.ResetOnSpawn = false
gui.Parent = player:WaitForChild("PlayerGui")
local button = Instance.new("TextButton")
button.Name = "RequestButton"
button.AnchorPoint = Vector2.new(1, 1)
button.Position = UDim2.new(1, -20, 1, -100)
button.Size = UDim2.fromOffset(220, 56)
button.BackgroundColor3 = Color3.fromRGB(30, 100, 60)
button.TextColor3 = Color3.new(1, 1, 1)
button.TextSize = 22
button.Text = "Request boost"
button.Parent = gui
local status = Instance.new("TextLabel")
status.Name = "StatusLabel"
status.AnchorPoint = Vector2.new(1, 1)
status.Position = UDim2.new(1, -20, 1, -164)
status.Size = UDim2.fromOffset(260, 76)
status.BackgroundColor3 = Color3.fromRGB(25, 30, 40)
status.TextColor3 = Color3.new(1, 1, 1)
status.TextSize = 18
status.TextWrapped = true
status.Text = "Stand near the green station and request a boost"
status.Parent = gui
local REQUEST_TIMEOUT = 4
local lastClick = -math.huge
local latestRequestId = 0
local pending = false
button.Activated:Connect(function()
if pending or os.clock() - lastClick < 0.6 then return end
lastClick = os.clock()
latestRequestId += 1
local requestId = latestRequestId
pending = true
button.Text = "Waiting..."
status.Text = "Requesting..."
request:FireServer("speed", requestId)
task.delay(REQUEST_TIMEOUT, function()
if pending and latestRequestId == requestId then
pending = false
button.Text = "Request boost"
status.Text = "No reply yet. Check your status, then tap to retry."
status.TextColor3 = Color3.fromRGB(255, 205, 160)
end
end)
end)
feedback.OnClientEvent:Connect(function(requestId, ok, message)
if requestId ~= latestRequestId then return end
pending = false
button.Text = "Request boost"
status.Text = message
status.TextColor3 = ok and Color3.fromRGB(180, 255, 190)
or Color3.fromRGB(255, 205, 160)
end)
Read the security boundary
The gameplay request is the string speed, plus a bounded integer request ID used only to match feedback. Speed, duration, score threshold, and target player all come from server-owned decisions. The server rate limiter rejects excessive attempts before doing more work. The client limiter makes normal clicks pleasant; it is not a security control because a modified client can ignore it. The client allows one pending request and shows a manual retry hint after four seconds without matching feedback. It never automatically resends. A timeout does not prove the server rejected the request. Correlation IDs prevent an older reply or timer from overwriting the newest request's status.
Next, the handler checks a live character, station proximity, the server's coin balance, and cooldown. The active token identifies this particular activation so a delayed callback cannot undo a later activation. On player removal, per-player tables are cleared. On character replacement, the callback does not apply an old character's speed to a new humanoid.
This is an authorization example, not a complete anti-cheat system. A player's movement can be client-controlled. A distance check does not prove they reached the station legitimately, and setting WalkSpeed on the server does not stop a malicious client from changing its movement. Before adding valuable rewards or competitive rankings, study Roblox's movement validation guidance and validate plausible progress. Avoid promising that a single remote validation function makes a game “exploit-proof.”
Acceptance tests
- With zero coins, walk from Start along positive Z onto BoostPlatform without touching a coin, then click. Expect the collect-coins message and unchanged speed.
- Collect three different coins. Far from the station, click. Expect the proximity message. Return over the course to Platform0, then walk onto BoostPlatform; confirm the side platform is reachable both before and after collection.
- Approach and click. Expect active feedback and faster walking. Coins should remain unchanged.
- Click again after one second. Expect cooldown feedback, not another activation.
- After five seconds, speed returns to its previous value. After twenty seconds from activation, another boost is allowed.
- Reset during the boost. The new character should use its normal spawn speed.
- With two clients, activate on one. The other's eligibility, cooldown, and speed should remain independent.
For a negative-input test, temporarily change FireServer("speed", requestId) to FireServer(123, requestId). Expect “Unknown request” and no gameplay change, then restore it. To test the timeout in a disposable test copy, temporarily comment out the client's FireServer line, click once, and wait four seconds. Expect the manual retry hint and no automatic requests; restore the line afterward. Do not add arbitrary privileged debug remotes. Code was statically reviewed, not Studio-executed for this manual.
Troubleshooting and exercises
- No button: the client may be waiting for BoostRemotes because BoostServer failed. Read server Output first.
- Duplicate-server assertion: delete the duplicate BoostServer rather than removing the guard. Remotes should have one authoritative handler.
- Boost never ends: check Output, duplicate scripts, and other code writing WalkSpeed. The restore deliberately avoids overwriting a different speed another system set meanwhile.
- Always too far away: inspect the station's real Position and character root in the server view.
Exercises: change the threshold to two coins, then test both sides of that boundary. Add a separate cosmetic accepted-effect on the client without changing score or speed there. Before supporting multiple overlapping movement effects, design one server movement controller instead of stacking independent delayed resets.
Sources and navigation
Last checked 2026-10-03: remotes, securing the boundary, Humanoid WalkSpeed, GuiButton.
Previous: 09 Client-server boundary · Next: 11 Modules and configuration
Related: 13 Cross-device controls · 14 Testing multiplayer · 16 Debugging and performance
Linked from
Sources
- https://create.roblox.com/docs/scripting/events/remote
- https://create.roblox.com/docs/reference/engine/classes/GuiButton
- https://create.roblox.com/docs/scripting/security/network-ownership
- https://create.roblox.com/docs/scripting/security/client-server-boundary
- https://create.roblox.com/docs/reference/engine/classes/Humanoid