In short
A Script runs on the server by default, a LocalScript runs on one player's device (the client), and a ModuleScript does not run by itself: it runs when another script calls require() on it, on whichever side that script is.
- Put Scripts in ServerScriptService. Put LocalScripts in StarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack or ReplicatedFirst. A LocalScript placed in a part in Workspace, or in ReplicatedStorage, never runs.
- Anything that matters, such as coins, damage or saving, belongs in a server Script. A change made by a LocalScript is seen by that one player only.
- Keep shared code in a ModuleScript in ReplicatedStorage. Keep server-only code in ServerScriptService or ServerStorage, which players never receive.
On this page
The client and the server in plain words
A running Roblox game is one server plus one client per player. The server is a Roblox computer that holds the real copy of the game and decides what happens. Each client is a player's device, running its own copy so it can draw the world and react to input at once.
Roblox keeps the copies in step through replication: when the server changes something players can see, every client gets the change. It does not work the other way. A change a client makes stays on that device, with a few exceptions.
When you press Play in Studio, both sides start on your computer. During the playtest, the Client/Server toggle in Studio's testing controls switches the Explorer and the viewport between them: a quick way to see which side a change happened on.
Which script type to use
Start from what the code has to do:
| The code... | Use | Put it in | Runs on | Can reach |
|---|---|---|---|---|
| Decides game rules: rewards, damage, rounds, saving | Script | ServerScriptService | The server | Everything, including ServerStorage and data stores |
| Belongs to one part, such as a kill brick | Script | Inside that part in Workspace | The server | Everything; the part is script.Parent |
| Reacts to one player's input or camera | LocalScript | StarterPlayerScripts | That player's device | Their input and camera, and whatever has replicated to them |
| Drives an on-screen interface | LocalScript | Inside the ScreenGui in StarterGui | That player's device | Their copy of the interface, in PlayerGui |
| Should start again each time the character spawns | LocalScript | StarterCharacterScripts | That player's device | The new character, as script.Parent |
| Shows a loading screen | LocalScript | ReplicatedFirst | That player's device, first | ReplicatedFirst; anything else only with WaitForChild |
| Is shared by server and client, such as settings | ModuleScript | ReplicatedStorage | Whichever side requires it | Whatever that side can reach |
| Is reused by server code only, such as saving helpers | ModuleScript | ServerScriptService or ServerStorage | The server | Everything the server can reach |
The rule behind the table: anything other players must see, or a cheater would want to change, is decided by a server Script. A LocalScript can only ask for it, through a RemoteEvent.
Which containers run which scripts
Location decides whether a script runs at all, and Roblox does not warn you when it never will. The Script column assumes the default RunContext, Legacy.
| Container | LocalScript | Script (Legacy) | Sent to players? |
|---|---|---|---|
| Workspace | Only inside a player's character | Runs | Yes |
| ServerScriptService | Never | Runs | Never |
| ServerStorage | Never | Never (server scripts can still require modules from it) | Never |
| ReplicatedStorage | Never | Never (it runs if RunContext is Client or Server) | Yes |
| ReplicatedFirst | Runs, before the rest of the game loads | Never (it runs if RunContext is Client) | Yes, first |
| StarterPlayerScripts | Runs, as a copy in the player's PlayerScripts | Never | A copy for each player |
| StarterCharacterScripts | Runs, as a copy inside each new character | Runs, as a copy inside each new character | A copy in each character |
| StarterGui | Runs, as a copy in the player's PlayerGui | Not documented for PlayerGui: keep server code in ServerScriptService | A copy for each player |
| StarterPack | Runs, as a copy in the player's Backpack | Inside a Tool, it runs at least while the tool is equipped, because the tool then sits in the character | A copy for each player |
Roblox's reference gives the Script rule: a Legacy Script starts once it is inside Workspace or ServerScriptService and Enabled is on. Characters live in Workspace, so Scripts copied from StarterCharacterScripts, or inside an equipped Tool, run on the server.
Why a LocalScript in Workspace does not run
A LocalScript runs for one particular player, so Roblox only runs it from places that belong to one player: their PlayerScripts, PlayerGui, Backpack and character, plus ReplicatedFirst, which each client loads for itself. A part in Workspace belongs to everyone, so a LocalScript inside it has no player to run for. The character is the exception: it sits in Workspace but belongs to one player.
Move the LocalScript to StarterPlayerScripts and find the part from there, for example with workspace:WaitForChild("Door"). If the change should reach everyone, use a server Script instead.
RunContext: Legacy, Server and Client
Only a Script has a RunContext property. Select the Script and change it in the Properties window.
| RunContext | Runs on | Runs from |
|---|---|---|
| Legacy (the default) | The server | Workspace and ServerScriptService only |
| Server | The server | ServerScriptService and Workspace, and also ReplicatedStorage, which Roblox advises against because every player receives its contents |
| Client | Each player's device | Containers the client receives, such as ReplicatedStorage, ReplicatedFirst and Workspace |
Roblox's script locations page now recommends one Server Script in ServerScriptService, one Client Script in ReplicatedStorage, and everything else in ModuleScripts they require. It suggests using LocalScripts sparingly. They are not deprecated, and this page uses them because they are the simplest place to start.
LocalPlayer, and why it is nil on the server
Players.LocalPlayer is the Player whose device is running the code. In a LocalScript, and in any module a LocalScript requires, it is set.
local Players = game:GetService("Players")
local player = Players.LocalPlayer -- the player on this device
print(`This copy of the script belongs to {player.Name}`)
On the server it is nil: the server runs for every player at once, so there is no "local" player. A Script that reads Players.LocalPlayer.Name fails with attempt to index nil with 'Name'. Server code gets the player from the event instead.
local Players = game:GetService("Players")
print("LocalPlayer on the server:", Players.LocalPlayer) --> nil
local function onPlayerAdded(player: Player)
print(`{player.Name} joined`) -- the event tells the server which player
end
Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do -- anyone who joined before this line ran
onPlayerAdded(player)
end
Players.PlayerAddedandPlayers.PlayerRemovingpass the player.RemoteEvent.OnServerEventalways passes the player who fired it, as the first argument. Roblox fills it in; the client cannot choose it.ProximityPrompt.TriggeredandClickDetector.MouseClickpass the player who used them.- In a
Touchedhandler,Players:GetPlayerFromCharacter(hit.Parent)finds the player whose character touched the part, or returns nil if it was not a character.
What replicates and what does not
| Made by | Example | Who sees it |
|---|---|---|
| The server | Moving a part, setting an attribute, changing leaderstats | Every player |
| The server | Anything inside ServerScriptService or ServerStorage | Only the server: these containers are never sent to players |
| A client | Changing a part, a value, an attribute or an interface | Only that player |
| A client | Moving its own character, animations it plays on it, and unanchored parts it has network ownership of | Every player: Roblox lets that device simulate the physics and passes the result on |
The last row is why Roblox's security docs warn that an exploiter can set their own WalkSpeed to any value, or fling parts they own. Two more things follow from how replication works:
- Roblox does not guarantee the order in which objects reach a client, so client code should use
WaitForChild. With streaming on, distant parts may not be there at all. - If a server Script sends an object from ServerStorage through a RemoteEvent, the client receives
nil, because that object never reached it.
ModuleScripts: require, return and caching
A ModuleScript runs only when a script calls require() on it, and it must return exactly one value that is not nil, usually a table. This one holds the settings for the example below.
-- Shared by RewardServer (which pays out) and RewardClient (which shows the timing).
-- Every player receives a copy of this module, so keep secrets out of it.
local RewardSettings = {
AMOUNT = 25, -- coins per claim
COOLDOWN = 30, -- seconds between claims
}
return RewardSettings
Either side loads it with require(ReplicatedStorage:WaitForChild("RewardSettings")).
A module runs once per side
The first require on a side runs the module and keeps what it returned. Later requires on that side get the same table, not a fresh copy: if one server Script changes RewardSettings.AMOUNT, every server Script sees it.
The server and each client have separate copies, so a change on the server never reaches a client's copy. Modules share code, not live data: to move data across, use a RemoteEvent or an attribute.
Where to keep modules
- ReplicatedStorage for modules both sides use. Every player receives these, and Roblox's security docs note they can be decompiled, so keep secrets and server-only logic out.
- ServerScriptService or ServerStorage for server-only modules, such as data store code. Clients never receive these: a LocalScript waiting for one shows
Infinite yield possible. - Avoid two modules that require each other. Roblox's docs say a circular require can fail with
Requested module was required recursivelyor leave the script hanging.
Example: one module, one Script, one LocalScript
Here all three types work together. The player presses R to claim 25 coins, at most once every 30 seconds. The client asks, the server checks the cooldown and pays, and the total replicates back. Coins live in an attribute so the example stands alone; a real game would use leaderstats, as in the leaderboard guide.
- Add the
RewardSettingsmodule above to ReplicatedStorage. - Add a Script named
RewardServerto ServerScriptService. - Add a LocalScript named
RewardClientto StarterPlayerScripts, inside StarterPlayer. - Press Play, press R, and watch Output.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Describes what RewardSettings returns, for Luau's type checker
type Settings = { AMOUNT: number, COOLDOWN: number }
local RewardSettings = require(ReplicatedStorage:WaitForChild("RewardSettings")) :: Settings
-- The server creates the RemoteEvent, so it exists before any client looks for it
local claimReward = Instance.new("RemoteEvent")
claimReward.Name = "ClaimReward"
claimReward.Parent = ReplicatedStorage
local lastClaim: { [Player]: number } = {}
local function onPlayerAdded(player: Player)
player:SetAttribute("Coins", 0) -- attributes set by the server replicate to clients
end
Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
onPlayerAdded(player)
end
-- Roblox passes the player who fired the event. Anything else a client sends is
-- ignored: the server decides the amount and the timing.
claimReward.OnServerEvent:Connect(function(player: Player)
local now = os.clock()
local last = lastClaim[player]
if last and now - last < RewardSettings.COOLDOWN then
return -- too soon, ignore the request
end
lastClaim[player] = now
local coins = player:GetAttribute("Coins")
if type(coins) == "number" then
player:SetAttribute("Coins", coins + RewardSettings.AMOUNT)
end
end)
Players.PlayerRemoving:Connect(function(player: Player)
lastClaim[player] = nil
end)
The :: Settings after require only tells Luau's strict type checker what the module returns. It changes nothing when the game runs.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
type Settings = { AMOUNT: number, COOLDOWN: number }
local RewardSettings = require(ReplicatedStorage:WaitForChild("RewardSettings")) :: Settings
local claimReward = ReplicatedStorage:WaitForChild("ClaimReward") :: RemoteEvent
local player = Players.LocalPlayer
local nextClaim = 0
UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessed: boolean)
if gameProcessed or input.KeyCode ~= Enum.KeyCode.R then
return -- typing in chat, or a different key
end
local now = os.clock()
if now < nextClaim then
print(`Next reward in {math.ceil(nextClaim - now)} seconds`)
return
end
nextClaim = now + RewardSettings.COOLDOWN
claimReward:FireServer() -- ask; the server decides
end)
-- The server changes the attribute, and the change replicates here
player:GetAttributeChangedSignal("Coins"):Connect(function()
print(`{player.Name} has {player:GetAttribute("Coins")} coins`)
end)
The client's cooldown check only gives feedback. The server repeats it, because a cheater can fire the RemoteEvent as often as they like. The RemoteEvents guide covers validation.
Try the Client/Server toggle while it runs: select your player under Players, and the Coins attribute in the Properties window shows the same total on both sides, because the server set it. Had the LocalScript set it, only the Client view would change.
How a well-organised place looks
No layout is required, but tidy places share a pattern: server code in ServerScriptService, shared modules in ReplicatedStorage, server-only assets in ServerStorage, client code in the Starter containers. This page's example is marked new.
Workspace
Map Folder
SpawnLocation SpawnLocation
ReplicatedFirst
LoadingScreen LocalScript
ReplicatedStorage
RewardSettings ModuleScript new
Shared Folder
Format ModuleScript
ServerScriptService
Leaderstats Script
RewardServer Script new
Server Folder
DataService ModuleScript
ServerStorage
Maps Folder
StarterGui
HUD ScreenGui
HUDController LocalScript
StarterPlayer
StarterPlayerScripts
RewardClient LocalScript new
StarterCharacterScripts
Footsteps LocalScript
Name scripts after what they do and group them in Folders. For many copies of one object, such as doors, Roblox suggests tagging them and handling them all from a single ModuleScript with CollectionService, not a script in every copy.
Common errors and fixes
These usually mean code is on the wrong side. Output can show and filter each message by context, Client or Server, and the script errors guide covers the messages in detail.
My LocalScript does nothing and shows no error
It is somewhere LocalScripts do not run: Workspace (outside a character), ReplicatedStorage, ServerScriptService or ServerStorage. Move it to StarterPlayerScripts, or into the ScreenGui in StarterGui if it drives an interface.
attempt to index nil with 'Name' in a server Script
The script read Players.LocalPlayer, which is nil on the server. Take the player from the event instead: PlayerAdded, the first argument of OnServerEvent, or GetPlayerFromCharacter.
It works for me, but other players do not see it
A LocalScript made the change, so it stayed on your device. Make the change in a server Script, and if a key press or button triggers it, fire a RemoteEvent and let the server act.
Infinite yield possible in a LocalScript
The client is waiting for something it will never receive, often a module or object in ServerScriptService or ServerStorage. Move shared modules and objects to ReplicatedStorage. See Infinite yield possible.
My Script runs twice
It is a Script with RunContext set to Client inside StarterPlayerScripts or StarterCharacterScripts, so the original and the player's copy both run. Move it to ReplicatedStorage, or replace it with a LocalScript.
Build it with RoCode
RoCode is an AI agent for Roblox Studio: you describe a change in a web chat, and its Studio plugin creates and edits scripts in the place you have open, each as a Script, LocalScript or ModuleScript in a container.
My coin reward only works for me. Move the logic to the server, keep the settings in one shared ModuleScript, and let the client ask with a RemoteEvent.
- Can find the existing reward code with Search Scripts and read it with Read Script, so it can move that logic rather than add a second copy
- Writes the Script, LocalScript and ModuleScript with Create Script or Edit Script, and can create the RemoteEvent with Create Instance and wire it between them in one run. By default it uses ServerScriptService and StarterPlayerScripts, or your own folders when the place has them
- Compile-checks each script with Check Script inside Studio before it finishes: a compile check, not a type check or a playtest
- Sends its changes in batches, and each batch is an undo point in Studio's history
RoCode does not start playtests, so press Play yourself and read the Server and Client lines in Output. RoCode is an independent tool, not made by Roblox. How RoCode connects to Studio.
Questions
Is a ModuleScript server-side or client-side?
Neither on its own. It runs on the side that requires it, separately on each side if both do. Where you store it decides who can require it: ReplicatedStorage for both sides, ServerScriptService or ServerStorage for the server only.
Can players read my scripts?
Assume an exploiter can read anything sent to their device. Roblox's security docs say LocalScripts, Client Scripts and ModuleScripts that reach a client can be decompiled, even if they never run. Scripts and ModuleScripts kept in ServerScriptService or ServerStorage never reach a client, so they cannot be.
Can a Script and a LocalScript share a variable?
No. They run in separate environments, one on the server and one on the player's device, even when Studio runs both on your computer. Pass data with a RemoteEvent, or set an attribute or value object on the server and read it on the client.
Sources and further reading
- Roblox Creator Docs: Script types and locations. script types, RunContext, the containers and Roblox's recommended layout
- Roblox Creator Docs: Reuse code. ModuleScripts, require, return values and one copy per side
- Roblox Creator Docs: Client-server runtime. the server, clients and replication
- Roblox Creator Docs: Data model. container services and what is copied to each player
- Roblox Engine API: BaseScript.RunContext. Legacy, Server and Client
- Roblox Engine API: Script. when a server Script starts running
- Roblox Engine API: Players.LocalPlayer. set on the client, nil on the server
- Roblox Creator Docs: Properties and attributes. replication order and WaitForChild
- Roblox Creator Docs: Access control and confidentiality. what clients receive and can decompile
- Roblox Creator Docs: Network ownership, movement validation, and physics. what a client controls for its own character and the parts it owns
- Roblox Creator Docs: Remote events and callbacks. the player argument on the server, and why non-replicated objects arrive as nil
- Roblox Creator Docs: Studio testing modes. the Client/Server toggle and colour-coded Output
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 30 September 2026). A type check catches misspelt APIs and wrong types, not game logic, so play-test in Studio before you publish.