Hungrit Docs

How scripts run#

What actually happens while your script is alive: when it wakes, when it sleeps, what Save really does, and where the ceilings are. Reading this once saves 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 two things:

  • the script being loaded, which means your file read top to bottom, once
  • one event firing, such as on_touch, on_timer or on_action

Between slices it does nothing and costs nothing. There is no loop running in the background and no "every frame" to hook into. If something should happen over and over, you ask for a timer.

Luau
local self = require("self") self:on_timer(1, function()    -- once a second, foreverend)

The body declares, the events act#

The top level of your file runs once, when the script loads. Use it to set things up: constants, handlers, timers.

Luau
--!strictlocal self = require("self") local SPEED = 2          -- a constant, fine herelocal visits = 0         -- a variable, fine here self:on_touch(function()  -- a handler, declared here and run later    visits += 1    print(visits, SPEED)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 still be loading.

If you need to act on the world at startup, positioning things, setting the weather, starting a round, use world:on_start. It runs after the last script in 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 really is ready straight away.

Saving restarts the script#

When you save a new version, the old one is thrown away completely and a fresh one starts.

local variablesgone
handlersgone, then declared again
timersgone, then armed again
self.storesurvives

That is the whole rule, and it is also why saving is the way to rescue 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 moving#

A region with nobody in it stops ticking. Your timers do not fire while it is asleep. When somebody arrives, it wakes up.

That matters for anything that should have made progress in the meantime: crops, ore coming back, a shop restocking. Two verbs exist for exactly this.

Luau
local world = require("world") -- Seconds this WORLD has lived. Keeps counting while the world is off.local now = world:clock()print(now) -- Fires when the world resumes after time nobody simulated.world:on_wake(function(elapsed)    print("nobody was here for " .. tostring(elapsed) .. " seconds")end)

The pattern that always works is to save a timestamp, not a countdown:

Luau
local self = require("self")local world = require("world") local function ripen() end -- when it was plantedself.store:set("planted", tostring(world:clock())) -- later, possibly after a restartlocal planted = tonumber(self.store:get("planted") or "0") or 0if world:clock() - planted >= 3600 then    ripen()end

A countdown only moves while somebody is there to tick it. A timestamp is true whether anyone was watching or not.

Catch up with arithmetic, never with a loop

elapsed can be a month, which is over two million seconds. for i = 1, elapsed do will exhaust your instruction budget and your script will be stopped. Divide instead.

When something goes wrong#

There are two very different outcomes, and telling them apart is most of debugging.

An ordinary mistake, such as indexing something that is nil, calling a method that does not exist, or doing arithmetic on text. The event is abandoned, the error goes to your console, and the script stays alive. The next click works fine.

Running away, meaning an endless loop or too much memory. The script is stopped and killed. It does not run again until you save it. You get one of these two messages:

MessageWhat happened
script budget exceededone slice used more than 50,000 instruction units
script memory limit exceededthe script went past its 4 MB

In practice both mean the same thing: something looped with no way out. Timers are the fix. One slice may not take long, but you can have as many slices as you like.

The ceilings#

These exist so one object cannot spoil a whole region. You will not meet most of them by accident.

Per slice, meaning one event or the load#

LimitValue
Instruction units50,000
World effects, such as moves, spawns and property writes8
Events sent with events:emit or 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
Size of one stored value64 KB
Store total1 MB

Per region#

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

The complete list, with every refusal message and what it means, is in Limits and refusals.

The eight effect rule, in practice#

The one ceiling you will genuinely feel is 8 world effects per event. Moving something, spawning something, writing properties, playing a sound: each of those is one effect.

Luau
local self = require("self")local world = require("world") self:on_touch(function()    for i = 1, 20 do        world:spawn({ shape = "sphere" })  -- the 9th one fails    endend)

The ninth call hands back nil, "too many commands". Nothing crashes. It simply does not happen, and the reason says why.

The fix is to spread the work across time:

Luau
local self = require("self")local world = require("world") local made = 0self: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 feel instant and others do not.

On the server goes everything that changes the world: moving objects, damage, spawning, sound, the store. Everyone sees the same result and nobody can edit their own copy to cheat.

On each player's own machine go the things that would feel broken with a delay:

  • menu navigation, sliders and toggles, where only a button that emits reports back to you
  • the crosshair
  • a weapon's recoil and the spread of its shot
  • the optional click preview, so a door starts moving on the frame of the click

The practical result is that a HUD with ten pages costs the server nothing, and a weapon's kick lands on the frame you pulled the trigger instead of a round trip later.

Next#

Updated 04 September 2026