Roblox Luau Scripting
Script a Roblox experience in Luau: services, Instances, events, the
server/client split, and secure cross-boundary communication. Targets the current
Roblox engine and Studio.
When to use
- Use when writing Roblox scripts: getting services, creating/parenting instances,
connecting events, deciding server vs client, or wiring
RemoteEvent/
RemoteFunction communication.
- Use when the project has
Script/LocalScript/ModuleScript objects, .rbxl(x)
places, or a Rojo *.project.json, and code calls game:GetService(...).
When not to use: persisting data across sessions → roblox-datastores.
Remote protocol architecture, exploit hardening, rate limits, high-frequency replication, and
multi-client abuse testing → roblox-networking. Generic Lua questions unrelated to the Roblox
API. Engine-agnostic input/save architecture → input-systems / save-systems.
Core workflow
- Get services with
game:GetService("Name"). Common ones: Players,
Workspace, ReplicatedStorage (shared client+server), ServerScriptService
(server-only code), ServerStorage, RunService, UserInputService (client).
- Know where code runs. A
Script runs on the server; a LocalScript
runs on a client (in StarterPlayerScripts, StarterGui, or the player's
character). A ModuleScript is shared code you require.
- Create instances deliberately.
local p = Instance.new("Part"), set its
properties, then set p.Parent last (parenting triggers replication).
- React with events.
:Connect to signals like Players.PlayerAdded,
part.Touched, or RunService.Heartbeat. Disconnect when done to avoid leaks.
- Cross the client/server boundary with Remotes — and never trust the client.
Clients request via
RemoteEvent:FireServer(...); the server validates and
applies. The server is authoritative for all game state.
- Test in Studio with Play / Play Here / server+client Start; use the Output
window and the server/client view toggle to confirm where code ran.
Patterns
1. Server Script: react to players joining (leaderstats)
-- ServerScriptService/Leaderboard.server.luau (a Script = runs on the server)
local Players = game:GetService("Players")
local function onPlayerAdded(player: Player)
local stats = Instance.new("Folder")
stats.Name = "leaderstats" -- this name makes it show on the leaderboard
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = stats
stats.Parent = player -- parent LAST
end
Players.PlayerAdded:Connect(onPlayerAdded)
2. Create and configure an instance
local Workspace = game:GetService("Workspace")
local part = Instance.new("Part")
part.Size = Vector3.new(4, 1, 4)
part.Position = Vector3.new(0, 10, 0)
part.Anchored = true -- won't fall under gravity
part.BrickColor = BrickColor.new("Bright blue")
part.Parent = Workspace -- set Parent last so it replicates once, fully
3. Connect an event (and disconnect to avoid leaks)
local debounce = false
local connection
connection = part.Touched:Connect(function(hit: BasePart)
local character = hit.Parent
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not humanoid or debounce then return end
debounce = true
humanoid.Health -= 10
task.wait(1) -- task.wait, NOT the deprecated wait()
debounce = false
end)
-- Later, when the part is removed or the round ends:
-- connection:Disconnect()
4. Client → server with a RemoteEvent (validate on the server!)
-- ReplicatedStorage: create a RemoteEvent named "BuyItem" (in Studio or via code).
-- CLIENT (LocalScript): request a purchase. The client can lie — this is only a request.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local buyItem = ReplicatedStorage:WaitForChild("BuyItem") -- wait: may not have replicated yet
buyButton.MouseButton1Click:Connect(function()
buyItem:FireServer("sword") -- send the item id only; never the price/result
end)
-- SERVER (Script): the ONLY place the transaction is decided.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local buyItem = ReplicatedStorage:WaitForChild("BuyItem")
local PRICES = { sword = 100, shield = 75 }
buyItem.OnServerEvent:Connect(function(player: Player, itemId)
-- TRUST NOTHING from the client. Validate types and values.
if type(itemId) ~= "string" then return end
local price = PRICES[itemId]
if not price then return end -- unknown item
local coins = player.leaderstats.Coins
if coins.Value < price then return end -- can't afford
coins.Value -= price -- server applies the change
grantItem(player, itemId)
end)
5. A per-frame loop with RunService
local RunService = game:GetService("RunService")
-- Heartbeat fires every frame AFTER physics; dt is seconds since the last step.
RunService.Heartbeat:Connect(function(dt)
spinner.CFrame *= CFrame.Angles(0, math.rad(90) * dt, 0) -- 90deg/sec, frame-independent
end)
6. Shared code in a ModuleScript
-- ReplicatedStorage/GameConfig (a ModuleScript) — usable by server and client.
local GameConfig = {}
GameConfig.MaxHealth = 100
function GameConfig.damageFor(weapon: string): number
return ({ sword = 25, bow = 15 })[weapon] or 0
end
return GameConfig
local GameConfig = require(game:GetService("ReplicatedStorage"):WaitForChild("GameConfig"))
print(GameConfig.MaxHealth)
Pitfalls
- Trusting the client is an exploit → clients can send any arguments to a
RemoteEvent/RemoteFunction. Validate every argument's type and range on the
server and keep the server authoritative over health, currency, and inventory.
LocalScript doesn't run where you put it → LocalScripts run in
StarterPlayerScripts, StarterCharacterScripts, StarterGui, or tools — not in
Workspace or ServerScriptService. Server Scripts belong in
ServerScriptService/Workspace.
- Deprecated globals → use
task.wait/task.spawn/task.delay, not the old
wait()/spawn()/delay() (worse scheduling and throttling).
- Parenting first, then setting properties → set properties first and
Parent
last so the instance replicates once in its final state.
nil on the client right after join → objects stream/replicate over time; use
parent:WaitForChild("Name") instead of indexing directly on the client.
- Connections never disconnected → long-lived
:Connect handlers leak and can
fire on destroyed objects; store the connection and :Disconnect() (or use
Instance:GetAttributeChangedSignal/:Once where appropriate).
- Using a RemoteFunction where a RemoteEvent fits →
RemoteFunction blocks
waiting for a return and a malicious/slow client can stall the server; prefer
one-way RemoteEvents unless you genuinely need a reply.
References
- For the full client/server model (replication,
RemoteFunction vs RemoteEvent,
:WaitForChild timing, BindableEvent for same-context messaging, attributes,
CollectionService tags, and :Once/connection cleanup), read
references/client-server.md.
Related skills
roblox-datastores — persist player data across sessions (server-only).
roblox-networking — production remote contracts, server validation, rate limits, replication,
streaming, prediction, and multi-client testing.
save-systems — engine-agnostic persistence concepts.
game-ai / input-systems — portable AI and input patterns to implement in Luau.
1---2name: roblox-luau3description: Script a Roblox experience in Luau: get services, create and parent Instances, connect events, run server Scripts vs client LocalScripts, and communicate across the client/server boundary with RemoteEvents/RemoteFunctions (server-authoritative). Use when building or debugging Roblox Studio scripts — when the user mentions Roblox, Luau, services, RemoteEvent, Instance.new, PlayerAdded, or client vs server. For saving player data use roblox-datastores.4---5
6# Roblox Luau Scripting
7
8Script a Roblox experience in **Luau**: services, `Instance`s, events, the
9server/client split, and secure cross-boundary communication. Targets the current
10Roblox engine and Studio.
11
12## When to use
13
14- Use when writing Roblox scripts: getting services, creating/parenting instances,
15 connecting events, deciding server vs client, or wiring `RemoteEvent`/
16 `RemoteFunction` communication.
17- Use when the project has `Script`/`LocalScript`/`ModuleScript` objects, `.rbxl(x)`
18 places, or a Rojo `*.project.json`, and code calls `game:GetService(...)`.
19
20**When *not* to use:** persisting data across sessions → `roblox-datastores`.
21Remote protocol architecture, exploit hardening, rate limits, high-frequency replication, and
22multi-client abuse testing → `roblox-networking`. Generic Lua questions unrelated to the Roblox
23API. Engine-agnostic input/save architecture → `input-systems` / `save-systems`.
24
25## Core workflow
26
271. **Get services with `game:GetService("Name")`.** Common ones: `Players`,
28 `Workspace`, `ReplicatedStorage` (shared client+server), `ServerScriptService`
29 (server-only code), `ServerStorage`, `RunService`, `UserInputService` (client).
302. **Know where code runs.** A `Script` runs on the **server**; a `LocalScript`
31 runs on a **client** (in `StarterPlayerScripts`, `StarterGui`, or the player's
32 character). A `ModuleScript` is shared code you `require`.
333. **Create instances deliberately.** `local p = Instance.new("Part")`, set its
34 properties, then set `p.Parent` **last** (parenting triggers replication).
354. **React with events.** `:Connect` to signals like `Players.PlayerAdded`,
36 `part.Touched`, or `RunService.Heartbeat`. Disconnect when done to avoid leaks.
375. **Cross the client/server boundary with Remotes — and never trust the client.**
38 Clients request via `RemoteEvent:FireServer(...)`; the server validates and
39 applies. The server is authoritative for all game state.
406. **Test in Studio** with Play / Play Here / server+client Start; use the Output
41 window and the server/client view toggle to confirm where code ran.
42
43## Patterns
44
45### 1. Server Script: react to players joining (leaderstats)
46
47```lua
48-- ServerScriptService/Leaderboard.server.luau (a Script = runs on the server)
49local Players = game:GetService("Players")
50
51local function onPlayerAdded(player: Player)
52 local stats = Instance.new("Folder")
53 stats.Name = "leaderstats" -- this name makes it show on the leaderboard
54
55 local coins = Instance.new("IntValue")
56 coins.Name = "Coins"
57 coins.Value = 0
58 coins.Parent = stats
59
60 stats.Parent = player -- parent LAST
61end
62
63Players.PlayerAdded:Connect(onPlayerAdded)
64```
65
66### 2. Create and configure an instance
67
68```lua
69local Workspace = game:GetService("Workspace")
70
71local part = Instance.new("Part")
72part.Size = Vector3.new(4, 1, 4)
73part.Position = Vector3.new(0, 10, 0)
74part.Anchored = true -- won't fall under gravity
75part.BrickColor = BrickColor.new("Bright blue")
76part.Parent = Workspace -- set Parent last so it replicates once, fully
77```
78
79### 3. Connect an event (and disconnect to avoid leaks)
80
81```lua
82local debounce = false
83local connection
84connection = part.Touched:Connect(function(hit: BasePart)
85 local character = hit.Parent
86 local humanoid = character and character:FindFirstChildOfClass("Humanoid")
87 if not humanoid or debounce then return end
88 debounce = true
89 humanoid.Health -= 10
90 task.wait(1) -- task.wait, NOT the deprecated wait()
91 debounce = false
92end)
93
94-- Later, when the part is removed or the round ends:
95-- connection:Disconnect()
96```
97
98### 4. Client → server with a RemoteEvent (validate on the server!)
99
100```lua
101-- ReplicatedStorage: create a RemoteEvent named "BuyItem" (in Studio or via code).
102-- CLIENT (LocalScript): request a purchase. The client can lie — this is only a request.
103local ReplicatedStorage = game:GetService("ReplicatedStorage")
104local buyItem = ReplicatedStorage:WaitForChild("BuyItem") -- wait: may not have replicated yet
105buyButton.MouseButton1Click:Connect(function()
106 buyItem:FireServer("sword") -- send the item id only; never the price/result
107end)
108```
109
110```lua
111-- SERVER (Script): the ONLY place the transaction is decided.
112local ReplicatedStorage = game:GetService("ReplicatedStorage")
113local buyItem = ReplicatedStorage:WaitForChild("BuyItem")
114local PRICES = { sword = 100, shield = 75 }
115
116buyItem.OnServerEvent:Connect(function(player: Player, itemId)
117 -- TRUST NOTHING from the client. Validate types and values.
118 if type(itemId) ~= "string" then return end
119 local price = PRICES[itemId]
120 if not price then return end -- unknown item
121 local coins = player.leaderstats.Coins
122 if coins.Value < price then return end -- can't afford
123 coins.Value -= price -- server applies the change
124 grantItem(player, itemId)
125end)
126```
127
128### 5. A per-frame loop with RunService
129
130```lua
131local RunService = game:GetService("RunService")
132-- Heartbeat fires every frame AFTER physics; dt is seconds since the last step.
133RunService.Heartbeat:Connect(function(dt)
134 spinner.CFrame *= CFrame.Angles(0, math.rad(90) * dt, 0) -- 90deg/sec, frame-independent
135end)
136```
137
138### 6. Shared code in a ModuleScript
139
140```lua
141-- ReplicatedStorage/GameConfig (a ModuleScript) — usable by server and client.
142local GameConfig = {}
143GameConfig.MaxHealth = 100
144function GameConfig.damageFor(weapon: string): number
145 return ({ sword = 25, bow = 15 })[weapon] or 0
146end
147return GameConfig
148```
149
150```lua
151local GameConfig = require(game:GetService("ReplicatedStorage"):WaitForChild("GameConfig"))
152print(GameConfig.MaxHealth)
153```
154
155## Pitfalls
156
157- **Trusting the client is an exploit** → clients can send any arguments to a
158 `RemoteEvent`/`RemoteFunction`. Validate every argument's type and range on the
159 server and keep the server authoritative over health, currency, and inventory.
160- **`LocalScript` doesn't run where you put it** → LocalScripts run in
161 `StarterPlayerScripts`, `StarterCharacterScripts`, `StarterGui`, or tools — not in
162 `Workspace` or `ServerScriptService`. Server `Script`s belong in
163 `ServerScriptService`/`Workspace`.
164- **Deprecated globals** → use `task.wait`/`task.spawn`/`task.delay`, not the old
165 `wait()`/`spawn()`/`delay()` (worse scheduling and throttling).
166- **Parenting first, then setting properties** → set properties first and `Parent`
167 last so the instance replicates once in its final state.
168- **`nil` on the client right after join** → objects stream/replicate over time; use
169 `parent:WaitForChild("Name")` instead of indexing directly on the client.
170- **Connections never disconnected** → long-lived `:Connect` handlers leak and can
171 fire on destroyed objects; store the connection and `:Disconnect()` (or use
172 `Instance:GetAttributeChangedSignal`/`:Once` where appropriate).
173- **Using a RemoteFunction where a RemoteEvent fits** → `RemoteFunction` blocks
174 waiting for a return and a malicious/slow client can stall the server; prefer
175 one-way `RemoteEvent`s unless you genuinely need a reply.
176
177## References
178
179- For the full client/server model (replication, `RemoteFunction` vs `RemoteEvent`,
180 `:WaitForChild` timing, `BindableEvent` for same-context messaging, attributes,
181 `CollectionService` tags, and `:Once`/connection cleanup), read
182 `references/client-server.md`.
183
184## Related skills
185
186- `roblox-datastores` — persist player data across sessions (server-only).
187- `roblox-networking` — production remote contracts, server validation, rate limits, replication,
188 streaming, prediction, and multi-client testing.
189- `save-systems` — engine-agnostic persistence concepts.
190- `game-ai` / `input-systems` — portable AI and input patterns to implement in Luau.