09 The client-server boundary
Before you start and what you will learn
Use the existing obby place, with the HUD from UI and client scripts. This lesson is an experiment, not another permanent game feature. You will inspect the same named object from the server and clients, change a local copy, and observe which changes cross the network.
A Roblox multiplayer session has one server simulation and separate client simulations. Roblox replicates relevant server state to connected clients. Clients render the experience, handle input, and may simulate physics they own. The distinction is more useful than assuming every object under Workspace is a single shared memory location. See Roblox's client-server runtime overview.
Decide responsibilities before writing remotes
For this project, the server owns collectible eligibility, awarded Coins, checkpoint progression, and whether a requested boost is allowed. A client owns its HUD layout, local button interactions, and purely cosmetic feedback. Other players do not need a network message whenever your HUD changes color.
ReplicatedStorage is a place both sides can see. It is not a private server vault, and putting a ModuleScript there does not make its runtime tables shared memory. ServerScriptService holds the server implementation. This mental division makes it easier to notice dangerous designs such as a client sending “my new balance is 10000.” The useful request is an intent, such as “try to activate the speed boost.” The server computes the result.
Add two temporary scripts
Leave the earlier scripts in place. Add these exact objects:
ServerScriptService
BoundaryServer Script
StarterPlayer
StarterPlayerScripts
BoundaryClient LocalScript
Do not pre-create BoundaryMarker or OnlyOnThisClient. The following code creates them during Play. Keep the marker near the spawn while doing this experiment so distance/streaming does not obscure what you are observing.
Server script
Download 09-boundary-server.server.luau
-- Temporary Script: ServerScriptService > BoundaryServer
local marker = Instance.new("Part")
marker.Name = "BoundaryMarker"
marker.Size = Vector3.new(4, 4, 4)
marker.Position = Vector3.new(0, 8, 0)
marker.Anchored = true
marker.CanCollide = false
marker.Color = Color3.fromRGB(255, 170, 0)
marker:SetAttribute("OwnerLabel", "server")
marker.Parent = workspace
while marker.Parent do
print("SERVER:", marker:GetAttribute("OwnerLabel"))
task.wait(3)
end
Client script
Download 09-boundary-client.client.luau
-- Temporary LocalScript: StarterPlayer > StarterPlayerScripts > BoundaryClient
local Players = game:GetService("Players")
local marker = workspace:WaitForChild("BoundaryMarker")
marker.Color = Color3.fromRGB(70, 150, 255)
marker:SetAttribute("OwnerLabel", "client " .. Players.LocalPlayer.Name)
print("CLIENT:", marker:GetAttribute("OwnerLabel"))
local localMarker = Instance.new("Part")
localMarker.Name = "OnlyOnThisClient"
localMarker.Size = Vector3.new(2, 2, 2)
localMarker.Position = Vector3.new(6, 8, 0)
localMarker.Anchored = true
localMarker.CanCollide = false
localMarker.Color = Color3.fromRGB(100, 255, 150)
localMarker.Parent = workspace
Inspect three different viewpoints
Start a Studio test with one server and two clients. If the current Studio layout differs, use the test mode selector to choose Server & Clients and two clients; the current Studio testing modes guide explains the controls. A solo Play session also exposes a client/server toggle, but two clients make the separate runtimes easier to see. See Testing multiplayer for a repeatable testing routine.
On each client, BoundaryMarker is blue. Its OwnerLabel Attribute contains that client's player name. Each client also sees a green OnlyOnThisClient Part. On the server, the original marker remains orange, its Attribute remains server, and there is no green Part. The server Output should print SERVER: server repeatedly. Client Output prints its local label once.
Both clients happen to run identical code, so both see blue and green objects. That does not mean client one replicated them to client two. The differing player names in OwnerLabel provide a clearer test. Disable BoundaryClient before a later run and both local effects disappear; no server state was saved by those edits.
This experiment uses an anchored Part's appearance and a local Attribute. Do not generalize it to “nothing a client does can affect the server.” Character movement, network-owned physics, and explicitly sent remote messages are important exceptions and attack surfaces. Roblox's network ownership and movement security guide explains why server-side reward code still needs context and movement checks.
Replication is state delivery, not a transaction system
A client can encounter an object before another related object is available. Use WaitForChild for startup dependencies, and design UI to tolerate temporarily missing display data. A RemoteEvent is an explicit message, while an Attribute or Value object is observable state. Neither turns multiple separate property changes into an atomic database transaction.
For a late-joining player's HUD, current replicated state is useful: it can read the current count immediately after the object arrives. A one-time “round started” event sent earlier is insufficient by itself because the new player was not present to handle it. Co-op rounds uses a small state object for this reason.
Verification and cleanup
Write down the server and each client's Attribute values, then compare them. Confirm the server never sees OnlyOnThisClient. This is your success criterion, rather than simply noticing a blue cube.
Stop the test. Delete the two temporary source scripts, BoundaryServer and BoundaryClient, before the next lesson. Runtime markers disappear when the test stops. Do not delete any original collectible/checkpoint scripts. These examples were statically reviewed; the observations above are expected results to verify in Studio.
Troubleshooting and exercises
- Server reports a client label: confirm you are actually inspecting the server viewport/Output and did not paste the client mutation into the server Script.
- The client waits forever: check server Output and the exact
BoundaryMarkername. Fix the cause rather than inserting an arbitrary startup sleep. - Two orange markers exist: look for duplicate BoundaryServer scripts.
Exercise: move the local green Part to a different position derived from LocalPlayer.UserId % 10. Predict whether the server can find it. Then explain, in your own words, why the CoinHud does not need a remote to read the server's current Coins value.
Sources and navigation
Last checked 2026-10-03: client-server runtime, properties and attributes, movement security, Studio test modes.
Previous: 08 UI and client scripts · Next: 10 Remotes and validation
Related: 02 Studio data model · 11 Modules and configuration · 14 Testing multiplayer