Appearance
How scripts run
This page explains what actually happens when your script is running: when it wakes up, when it sleeps, what happens when you press Save, and what the limits are. Reading it once saves you a lot of guessing later.
A script is asleep almost all the time
Your script runs in short bursts, called slices. A slice is one of:
- the script being loaded (your file, top to bottom, once)
- one event firing (
on_touch,on_timer,on_action…)
Between slices it does nothing and costs nothing. There is no loop running in the background, and there is no "every frame" for you to hook into — if you need something to happen repeatedly, you ask for a timer.
luau
self:on_timer(1, function()
-- runs once a second, forever
end)The body declares, the events act
The top level of your file runs once, when the script loads. Use it to set up: constants, handlers, timers.
luau
--!strict
local self = require("self")
local SPEED = 2 -- a constant: fine here
local visits = 0 -- a variable: fine here
self:on_touch(function() -- a handler: declared here, runs later
visits += 1
end)What the body cannot count on is the rest of the world existing yet. The region starts its scripts one object at a time, so while your body runs, other objects may not have loaded.
If you need to act on the world at startup — position things, set the weather, start a round — use world:on_start, which runs after the last script of the region has finished loading.
luau
local world = require("world")
world:on_start(function()
world:set_weather({ rain = 0.2 })
end)Attach a script to a world that is already running and on_start fires on the next tick, because for that script "the world is ready" is true immediately.
Saving restarts the script
When you save a new version, the old one is thrown away entirely and a fresh one starts:
- local variables are gone
- handlers are gone and re-declared
- timers are gone and re-armed
self.storesurvives
That is the whole rule. It also means saving is how you restart a script that has got itself into a strange state.
The one thing that runs before the old version dies is on_detach, and only on worn items — it is the last moment such a script can write to its store.
Regions sleep, and time keeps passing
A region with nobody in it stops ticking. Your timers do not fire while it sleeps. When somebody arrives, it wakes up.
That matters for anything that should have progressed in the meantime — crops, respawning ore, a shop restocking. Two verbs exist for it:
luau
local world = require("world")
-- Seconds this WORLD has lived. Keeps counting while the world is off.
local now = world:clock()
-- Fires when the world resumes after unsimulated time.
world:on_wake(function(elapsed)
-- `elapsed` is how many seconds went by with nobody here
end)The reliable pattern is to save a timestamp, not a countdown:
luau
-- when it was planted
self.store:set("planted", tostring(world:clock()))
-- later, possibly after a restart
local planted = tonumber(self.store:get("planted") or "0") or 0
if world:clock() - planted >= 3600 then
ripen()
endA countdown only moves while somebody is ticking it. A timestamp is true whether anyone was there or not.
Catch up with arithmetic, not with a loop. elapsed can be a month — over two million seconds. for i = 1, elapsed do will exhaust your instruction budget and your script will be stopped. Divide instead.
What happens when something goes wrong
There are two very different outcomes, and telling them apart is most of debugging.
An ordinary mistake — indexing something that is nil, calling a method that does not exist, arithmetic on text. The event is abandoned, the error is written to your console, and the script stays alive. The next click works.
Running away — an endless loop, or using too much memory. The script is stopped and killed. It does not run again until you save it. You get one of these:
| Message | Meaning |
|---|---|
script budget exceeded | one slice used more than 50,000 instruction units |
script memory limit exceeded | the script went over its 4 MB of memory |
Both mean the same thing in practice: something looped without an exit. Timers are the fix — a slice is not allowed to take a long time, but you can have as many slices as you like.
The ceilings
These exist so that one object cannot spoil a whole region. You will not meet most of them by accident.
Per slice (one event, or the load)
| Limit | Value |
|---|---|
| Instruction units | 50,000 |
| World effects (moves, spawns, property writes…) | 8 |
Events sent with events:emit / events:send | 16 |
print lines | 32 |
Length of one print line | 512 bytes |
Per script
| Limit | Value |
|---|---|
| Memory | 4 MB |
Keys written by one self:set call | 32 |
Per object
| Limit | Value |
|---|---|
| Scripts | 8 |
| Tags | 16 |
| Store keys | 256 |
| Store value size | 64 KB |
| Store total | 1 MB |
Per region
| Limit | Value |
|---|---|
| Active scripts | 768 |
| Objects created by scripts, alive at once | 256 |
| Named actions | 32 |
The full list, including every refusal message and what it means, is in Limits and refusals.
The eight-effect rule, in practice
The one ceiling you will actually feel is 8 world effects per event. Moving something, spawning something, writing properties, playing a sound — each is one effect.
luau
self:on_touch(function()
for i = 1, 20 do
world:spawn({ shape = "sphere" }) -- the 9th one fails
end
end)The ninth call returns nil, "too many commands". Nothing crashes; it simply does not happen, and the reason says so.
The fix is to spread the work over time:
luau
local made = 0
self:on_timer(0.2, function()
if made >= 20 then return end
made += 1
world:spawn({ shape = "sphere" })
end)Where each thing runs
You never choose this, but knowing it explains why some things are instant and others are not.
On the server — everything that changes the world. Moving objects, damage, spawning, sound, the store. Every player sees the same result, and nobody can edit their copy to cheat.
On each player's own machine — things that would feel broken with a delay:
- menu navigation, sliders, toggles (only a button with
emitreports back) - the crosshair
- a weapon's recoil and its shot cone
- the optional click preview (
predictonon_touch)
The practical consequence: a HUD with ten pages costs the server nothing, and a weapon's kick lands on the frame of the click rather than a round trip later.
Next
- Reading errors — what each message means and how to find the line
- Saving data — the store, in depth
- Limits and refusals — the complete table
