Skip to content
Lesson 11.7NPC Policy, Lua, and Script Execution

Script Budgets, Errors, and Recovery

Memory and instruction limits, tracebacks, and failure handling keep useful scripts from overrunning their host.

Lesson 11.7
Read or listenPrint chapter or book
About 30 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 11.7Key ideas, reference and questions

A one-page summary of “Script Budgets, Errors, and Recovery” you can keep beside your notes. Plain Markdown.

Lesson 11.7 — Script Budgets, Errors, and Recovery: cheatsheet

  • Remember: A budget needs both a resource and a reset window, so the same limit has one clear meaning across callbacks and frames.
  • Also useful: Locate the failed layer first: unreadable syntax, unsuitable runtime values, rejected host requests, and wrong gameplay decisions need different repairs.

Reading map

  • A script can fail without being malicious
  • Budgets describe several resources
  • Limit memory
  • Limit instructions
  • See the budget fail on purpose
  • Protect every host callback
  • Make errors useful but not secret-leaking
  • Use a failure policy
  • Test more than the happy path
  • Sandboxing is a design, not one switch

Review

  • Q: Why is an instruction budget not a complete Lua sandbox? A: Memory, time, callbacks and actions each need limits
  • Q: What stance should an embedded interpreter take toward script failure? A: Treat it as a normal result; keep the game running
Download cheatsheet (.md)

Suppose that helper script encounters a missing unit halfway through a turn. The game still needs to draw the next frame and let the player regain control. The script host therefore needs a clear error report, a bound on work, and a safe way to disable the failing script.

Common mistakes include:

  • an infinite loop;
  • a table that grows every update;
  • calling a missing function;
  • assuming an entity always exists;
  • dividing by zero and producing a non-finite value;
  • repeatedly requesting an action the host rejects.

An embedded interpreter must treat failure as a normal result. One script error should not crash the game, leak resources, or leave input stuck.

Before setting limits, locate where the failure occurred. A syntax error means the source never became a runnable function. A runtime type error means execution reached unsuitable values. A host-API error means the script called the engine but violated that function’s contract. A gameplay error may raise no exception at all: every layer ran, but the requested action was wrong.

stops here
stops here
stops here
wrong result
Source text
Syntax check
Syntax error:
no runnable function
Lua runtime executes
Runtime type error:
unsuitable values
Host function
Host-API error:
contract violated
Game state changes
Gameplay error:
nothing is raised

The first three can produce diagnostic errors; the fourth needs an assertion about expected behavior. Keep that distinction when choosing what to log and what to test.

“The script is small” does not imply that it is cheap to run. Bound the dimensions separately:

  • source and bytecode size;
  • Lua-managed memory;
  • VM instructions per update;
  • call-frame depth;
  • host-callback calls and returned data;
  • wall-clock deadline for work outside instruction counting;
  • queued actions and log output.

Choose whether a budget resets per callback, per frame, or per whole script session. A limit with no time window is ambiguous. Record which budget stopped execution so a learner can distinguish an infinite loop from memory growth or excessive host calls.

Resource Guard Reset window
Source and bytecode Maximum input bytes Script load
Lua-managed memory Allocator limit Script session
VM instructions Hook counter One update
Call frames Maximum depth One call chain
Host callbacks Call and returned-byte caps One update
Wall-clock work Host deadline or cancellation One update
Actions and logs Queue and output-byte caps One update

When a limit is crossed, stop that update, discard its uncommitted actions, and report the exact budget. Do not quietly continue with half-produced output.

The lab host applies a four-mebibyte Lua allocation limit. The multiplication converts four mebibytes to bytes before the host sets the limit:

let lua = mlua::Lua::new_with(
mlua::StdLib::TABLE | mlua::StdLib::STRING | mlua::StdLib::MATH,
mlua::LuaOptions::default(),
)?;
lua.set_memory_limit(4 * 1024 * 1024)?;

Choose a limit after measuring realistic scripts. Too low prevents normal work; unlimited growth lets one bug consume the process.

The limit covers Lua-managed allocations, not every resource a host callback might allocate. Host functions need their own bounds.

mlua can call a host hook after a chosen number of VM instructions. The hook may run while Lua is executing, so its counter has to be shared safely with the Rust closure: Arc shares ownership of the counter, and Mutex protects one update to it at a time. The clone shares the same counter; it does not reset the budget.

use std::sync::{Arc, Mutex};
use mlua::{HookTriggers, VmState};
let calls = Arc::new(Mutex::new(0_u32));
let counter = Arc::clone(&calls);
lua.set_hook(
HookTriggers::new().every_nth_instruction(1_000),
move |_, _| {
let mut calls = counter.lock()
.map_err(|_| mlua::Error::RuntimeError("hook counter failed".into()))?;
*calls += 1;
if *calls > 100 {
return Err(mlua::Error::RuntimeError(
"script exceeded its instruction budget".into()
));
}
Ok(VmState::Continue)
},
)?;
no
yes
VM executes 1,000 instructions
host hook fires
calls += 1
calls > 100?
VmState::Continue
RuntimeError:
instruction budget exceeded

Since the hook fires every 1,000 instructions and allows at most 100 calls, a script gets roughly 100,000 VM instructions per update before this particular budget stops it.

The hook is not a stopwatch. Different instructions cost different amounts of real time, and host callbacks can do work between hooks. It is one defense against runaway Lua bytecode.

The host hook counts one call per 1,000 Lua instructions and raises an error on call 101, so a runaway loop is stopped after roughly 100,000 instructions per update. The interactive version needs JavaScript; the lesson text around it explains the same idea.

Predict first

The hook above fires every 1,000 instructions and raises an error when its count passes 100. Suppose the host never sets the count back to 0, and a well-behaved script runs 30,000 instructions in every update.

In which update does the instruction budget stop the script?

Answer: In update 4. 30,000 ÷ 1,000 = 30 hook calls per update. After three updates the count is 30 + 30 + 30 = 90; in update 4 the 11th call makes it 101, more than 100, so the error fires after 11,000 of that update’s instructions.

One instruction budget, two reset windowsThe hook counts one call per 1,000 Lua instructions and raises an error when the count passes 100. Choose when the host sets the count back to 0.

Hook calls, update by update

Hook calls counted

Calls in update #1, #2, …
30#1
30#2
30#3
11#4
–#5
Count (carried over)
30
60
90
101 > 100error
–

Result

Stopped in update 4: instruction budget exceeded

Each update runs 30,000 instructions, so the hook fires 30,000 ÷ 1,000 = 30 times. Never reset, the count carries over: 30, 60, 90. In update 4, call 11 makes the count 101, more than 100, so the error stops the script after 11,000 of that update’s instructions.

Reset the instruction allowance at a controlled host boundary, not from inside the script. If the hook counter is shared across updates, document that as a session budget; if each update gets a fresh allowance, also cap how often updates can be requested.

scripts/budget_failure.lua contains:

-- ❌ This intentional failure demonstrates the host's instruction budget.
while true do
end

Run it:

Terminal window
cargo run --manifest-path lua-labs/Cargo.toml -- scripts/budget_failure.lua

Expected result: the host returns a contextual error naming the script and the exceeded budget. If the terminal hangs forever, the limit is not working.

A function exposed to Lua should validate:

  • argument types and string lengths;
  • sequence lengths;
  • numeric finiteness and range;
  • referenced IDs;
  • permissions for the current script;
  • rate and action budgets.

Never hold a poisoned mutex or important engine lock while running arbitrary script code. Copy inputs first, execute, then validate collected outputs.

Development builds should report:

  • script path or stable identifier;
  • line and traceback when available;
  • host API function involved;
  • which type, range, permission, or action check failed;
  • whether the script was disabled or may retry.

Do not dump process memory, account tokens, full private chat logs, or arbitrary host paths into a public error report.

One practical policy is:

second error
repeated error
first error
stop this update,
report it
reset script state;
retry only on user request
disable the script
for the session
host invariant failure
stop all scripted
actions immediately

Do not catch an error and continue from half-mutated host state. Lua requests should be queued and committed only after the script returns successfully.

Create scripts that:

  • request an unknown action;
  • omit a required field;
  • pass a string where an ID is expected;
  • allocate an ever-growing table;
  • loop forever;
  • throw an error inside a helper function;
  • return normally with zero actions;
  • submit the maximum allowed valid action list.

For each test, assert the result, action count, error category, and whether the script remains enabled.

No single setting makes arbitrary scripts safe. The useful layers are:

  • a limited standard library;
  • no arbitrary native module loading;
  • a narrow host API;
  • copied snapshots;
  • structured action requests;
  • memory, instruction, time, and rate limits;
  • least-privilege Windows handles behind the host boundary;
  • clear cancellation and recovery.

Lua is most valuable when it makes small policies easy to change while the host keeps powerful capabilities narrow and testable. 🔒

Knowledge check

Limit different resources

Why is an instruction budget not a complete Lua sandbox?

Finished Lesson 11.7?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 11: 11 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.