Skip to content

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.store survives

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()
end

A 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:

MessageMeaning
script budget exceededone slice used more than 50,000 instruction units
script memory limit exceededthe 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)

LimitValue
Instruction units50,000
World effects (moves, spawns, property writes…)8
Events sent with events:emit / events:send16
print lines32
Length of one print line512 bytes

Per script

LimitValue
Memory4 MB
Keys written by one self:set call32

Per object

LimitValue
Scripts8
Tags16
Store keys256
Store value size64 KB
Store total1 MB

Per region

LimitValue
Active scripts768
Objects created by scripts, alive at once256
Named actions32

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 emit reports back)
  • the crosshair
  • a weapon's recoil and its shot cone
  • the optional click preview (predict on on_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

Hungrit scripting documentation.