atherhubather.hub
Back to Guides
Roblox development
8 min read
May 11, 2026

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.

Ather
Ather
Lead developer at Atherhub. Writes about Roblox internals, Luau, script engineering, and platform security.Last updated May 11, 2026

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).

luau
-- 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.

luau
-- 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.

Authority lives on the server
The most common newcomer bug: applying damage, awarding coins, or saving data from a LocalScript. These need to happen on the server via RemoteEvents. The LocalScript fires the event; the server validates and applies.

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.

luau
-- 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
luau
-- Any script:
local Math = require(game.ReplicatedStorage.Math)
print(Math.clamp(15, 0, 10))  --> 10
print(Math.lerp(0, 100, 0.25))  --> 25

ModuleScripts 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.
  • ServerStorage or ServerScriptService — 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 Workspace or ServerScriptService. If it's elsewhere, it sits dormant.
  • LocalScript: runs in PlayerGui, Character, Backpack, or copied via StarterPlayerScripts / 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.

luau
-- 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-lineUtil grab-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:Connect calls across many files.
  • Type modules with --!strict at 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
Written by Ather

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.