Server Scripts, LocalScripts, and ModuleScripts
Roblox has three flavours of script and they aren't interchangeable. The difference between them — server vs client vs library — is the single most important mental model in Roblox development, and getting it right early saves enormous amounts of debugging later.
Server Script (just 'Script')
Server scripts run on the game server. The server is the authoritative source of truth — anything that affects state shared between players (damage, currency, inventory, world changes) belongs here.
They live by default in ServerScriptService (where the engine auto-runs them on game start), but they also work in Workspace (attached to a part — useful when the script should be tied to that part's lifetime).
-- ServerScriptService/Damage.Script
local Players = game:GetService("Players")
local function damage(player, amount)
local char = player.Character
local h = char and char:FindFirstChildOfClass("Humanoid")
if h then h.Health = math.max(0, h.Health - amount) end
end
Players.PlayerAdded:Connect(function(p)
p.CharacterAdded:Connect(function() damage(p, 0) end)
end)The same Script can't run on the client. If you parent a Server Script under StarterPlayer.StarterPlayerScripts or a Player's GUI, it silently doesn't execute there.
LocalScript
LocalScripts run on one player's client. They handle everything that only that player should see or feel: input, camera, HUD, sound effects, particle bursts, local UI animations.
They run when parented to one of these locations: aPlayerGui, the player's Backpack, the player's Character, or StarterPlayer.StarterPlayerScripts (where they get copied into each player on join). They do not run from Workspace or ServerScriptService.
-- StarterPlayer/StarterPlayerScripts/Camera.LocalScript
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.F then
workspace.CurrentCamera.FieldOfView = 30
end
end)
UserInputService.InputEnded:Connect(function(input)
if input.KeyCode == Enum.KeyCode.F then
workspace.CurrentCamera.FieldOfView = 70
end
end)The most important consequence: LocalScripts have no authority. Anything they do is purely local to the player running them. A LocalScript that says "localPlayer.Character.Humanoid.Health = 100" only changes health on that one player's screen — the server doesn't see it, and other players don't see it.
ModuleScript
ModuleScripts are libraries. They don't run on their own — they exist to be require()d from other scripts. The thing they return becomes the value of the require expression.
-- ReplicatedStorage/Math.ModuleScript
local Math = {}
function Math.clamp(v: number, min: number, max: number): number
if v < min then return min end
if v > max then return max end
return v
end
function Math.lerp(a: number, b: number, t: number): number
return a + (b - a) * t
end
return Math-- Any script:
local Math = require(game.ReplicatedStorage.Math)
print(Math.clamp(15, 0, 10)) --> 10
print(Math.lerp(0, 100, 0.25)) --> 25ModuleScripts run on whichever side calls require() on them. If a Server Script requires a module, the module runs on the server. If a LocalScript requires the same module, it runs again on that client. A module is not a singleton across the network — it's a singleton per side.
Conventional places to put modules:
ReplicatedStorage— modules used by both sides. The most common location.ServerStorageorServerScriptService— server-only modules (datastore helpers, balance tables the client shouldn't see).StarterPlayerScripts— client-only modules (input mappers, local UI helpers).
How they actually run
Scripts execute when they enter a runnable location for the first time. "Runnable location" varies by type:
- Server Script: runs when its parent is in
WorkspaceorServerScriptService. If it's elsewhere, it sits dormant. - LocalScript: runs in
PlayerGui,Character,Backpack, or copied viaStarterPlayerScripts/StarterCharacterScripts/StarterGui. Always per-player. - ModuleScript: never on its own. Runs when something requires it.
The Disabled property on any script stops it from running. Toggling it back to false re-runs the script from the top.
Crossing the boundary: RemoteEvent / RemoteFunction
A pattern you'll use constantly: client wants something to happen, server has to validate and execute. The bridge is a RemoteEvent in ReplicatedStorage.
-- ReplicatedStorage/Buy.RemoteEvent
-- ServerScriptService/Buy.Script:
local Buy = game.ReplicatedStorage.Buy
local prices = { Sword = 100, Bow = 75 }
Buy.OnServerEvent:Connect(function(player, item)
if not prices[item] then return end
local coins = player:FindFirstChild("leaderstats") and player.leaderstats.Coins
if not coins or coins.Value < prices[item] then return end
coins.Value -= prices[item]
-- give item ...
end)
-- StarterPlayerScripts/Shop.LocalScript:
local Buy = game.ReplicatedStorage.Buy
Buy:FireServer("Sword")The LocalScript fires; the server validates the request against its authoritative state; if valid, it applies the effect. The client never has authority — even if the LocalScript is replaced or modified, the server still decides what happens.
Choosing the right script type
A short decision tree:
- Does it affect state other players will see? Server Script (or a server-side handler triggered by a client RemoteEvent).
- Does it react to this player's input or change only their view? LocalScript.
- Is it logic that both sides need? ModuleScript in ReplicatedStorage.
- Is it server-only logic too big to inline? ModuleScript in ServerStorage or ServerScriptService.
Conventions that scale
A handful of conventions make codebases readable as they grow:
- One module per concept. Don't make a 2000-line
Utilgrab-bag. - All RemoteEvents in a single folder in ReplicatedStorage, named for what they do (
BuyRequest,ShootFired), not how they're used. - Server-side handlers register themselves at script start; don't scatter
OnServerEvent:Connectcalls across many files. - Type modules with
--!strictat the top once their public API has stabilised.
Wrap-up
Server, Local, Module — three types, three roles. Get the boundary between server and client right and 90% of Roblox architecture problems disappear. The other 10% is mostly deciding where modules live, and that's a refactor problem, not a debugging problem.
Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.