Explainer · Scripting foundations

LocalScript vs Script vs ModuleScript in Roblox

A script that does nothing and shows no error is often fine: it is just somewhere Roblox never runs it. This page shows where each script type runs, what it can reach, and how to share code between them.

Updated 30 September 202610 min readBeginnerLuau

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
  1. Client and server
  2. Which script to use
  3. Where scripts run
  4. RunContext
  5. LocalPlayer
  6. What replicates
  7. ModuleScripts
  8. Full example
  9. Organising a place
  10. Common errors
  11. With RoCode
  12. Questions
  13. Sources

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:

Choosing a script type and a container
The code...UsePut it inRuns onCan reach
Decides game rules: rewards, damage, rounds, savingScriptServerScriptServiceThe serverEverything, including ServerStorage and data stores
Belongs to one part, such as a kill brickScriptInside that part in WorkspaceThe serverEverything; the part is script.Parent
Reacts to one player's input or cameraLocalScriptStarterPlayerScriptsThat player's deviceTheir input and camera, and whatever has replicated to them
Drives an on-screen interfaceLocalScriptInside the ScreenGui in StarterGuiThat player's deviceTheir copy of the interface, in PlayerGui
Should start again each time the character spawnsLocalScriptStarterCharacterScriptsThat player's deviceThe new character, as script.Parent
Shows a loading screenLocalScriptReplicatedFirstThat player's device, firstReplicatedFirst; anything else only with WaitForChild
Is shared by server and client, such as settingsModuleScriptReplicatedStorageWhichever side requires itWhatever that side can reach
Is reused by server code only, such as saving helpersModuleScriptServerScriptService or ServerStorageThe serverEverything 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.

Where each script type runs
ContainerLocalScriptScript (Legacy)Sent to players?
WorkspaceOnly inside a player's characterRunsYes
ServerScriptServiceNeverRunsNever
ServerStorageNeverNever (server scripts can still require modules from it)Never
ReplicatedStorageNeverNever (it runs if RunContext is Client or Server)Yes
ReplicatedFirstRuns, before the rest of the game loadsNever (it runs if RunContext is Client)Yes, first
StarterPlayerScriptsRuns, as a copy in the player's PlayerScriptsNeverA copy for each player
StarterCharacterScriptsRuns, as a copy inside each new characterRuns, as a copy inside each new characterA copy in each character
StarterGuiRuns, as a copy in the player's PlayerGuiNot documented for PlayerGui: keep server code in ServerScriptServiceA copy for each player
StarterPackRuns, as a copy in the player's BackpackInside a Tool, it runs at least while the tool is equipped, because the tool then sits in the characterA 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.

What each RunContext does
RunContextRuns onRuns from
Legacy (the default)The serverWorkspace and ServerScriptService only
ServerThe serverServerScriptService and Workspace, and also ReplicatedStorage, which Roblox advises against because every player receives its contents
ClientEach player's deviceContainers 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.

LocalScript in StarterPlayerScripts named WhoAmIClient
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.

Script in ServerScriptService named WhoAmIServer
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.PlayerAdded and Players.PlayerRemoving pass the player.
  • RemoteEvent.OnServerEvent always passes the player who fired it, as the first argument. Roblox fills it in; the client cannot choose it.
  • ProximityPrompt.Triggered and ClickDetector.MouseClick pass the player who used them.
  • In a Touched handler, 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

Who sees a change
Made byExampleWho sees it
The serverMoving a part, setting an attribute, changing leaderstatsEvery player
The serverAnything inside ServerScriptService or ServerStorageOnly the server: these containers are never sent to players
A clientChanging a part, a value, an attribute or an interfaceOnly that player
A clientMoving its own character, animations it plays on it, and unanchored parts it has network ownership ofEvery 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.

ModuleScript in ReplicatedStorage named RewardSettings
-- 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 recursively or 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.

  1. Add the RewardSettings module above to ReplicatedStorage.
  2. Add a Script named RewardServer to ServerScriptService.
  3. Add a LocalScript named RewardClient to StarterPlayerScripts, inside StarterPlayer.
  4. Press Play, press R, and watch Output.
Script in ServerScriptService named RewardServer
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.

LocalScript in StarterPlayerScripts named RewardClient
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.

Explorer: a tidy place. ClaimReward appears in ReplicatedStorage only while the game runs
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.

You type

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.

RoCode does
  • 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

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.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.