How to run Lua code in a sandbox
Install Lua 5.4 or LuaJIT in a Linux microVM, write the script there, and run it with lua5.4 under a time limit and with the network off.
On Runtime Lua runs in a microVM of its own, so there is no need to strip the
standard library to make it safe. Each sandbox is a Firecracker microVM running
Ubuntu 24.04.5, where sudo apt-get install -y lua5.4 gives you the interpreter
in one command. A new sandbox took 351 ms from the create request to its first
Python result at the median, measured on 24 September 2026
(speed).
In-process sandboxing versus a microVM
Lua is often embedded, and the usual way to run a stranger's Lua is to load it
with a restricted environment that leaves out os, io and require. That
guards the functions you remembered to remove. It does not bound CPU time or
memory by itself, and the code still runs inside your process.
| Concern | Restricted _ENV in your process |
A Runtime sandbox |
|---|---|---|
os.execute, io.open |
Removed by hand, one by one | Allowed; they reach only a throwaway machine |
| Endless loop | Needs a debug hook or a watchdog | timeoutMs ends the command |
| Memory | Needs a custom allocator | The sandbox's memoryMiB, enforced on the host |
| Network | Whatever your process can reach | Off with internet: false; private addresses refused |
| A bug in the interpreter | Runs with your process's rights | Contained by the microVM's own kernel |
If the Lua must call back into your application, keep an embedded interpreter for that part. For scripts that only compute, a sandbox is the simpler boundary (security).
Lua packages on Ubuntu 24.04
Lua is not in the default image. From packages.ubuntu.com, 25 September 2026:
| Package | Version on noble | Command |
|---|---|---|
lua5.4 |
5.4.6 | lua5.4, luac5.4 |
luajit |
2.1.0 (git 20231223) | luajit |
lua5.1 |
5.1.5 | lua5.1 |
luarocks |
3.8.0 | luarocks |
Run a script
Check the syntax first with luac5.4 -p, which loads the file without running
it (luac manual), then run it with
its input passed as arguments.
TypeScriptimport { Sandbox } from "withruntime";const script = `local n = tonumber(arg[1])local squares = {}for i = 1, n do squares[#squares + 1] = i * i endprint(table.concat(squares, ","))`;await using sbx = await Sandbox.create({ timeoutSeconds: 600, onLeaseEnd: "stop" });await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q lua5.4", { check: true, timeoutMs: 300_000,});await sbx.network.set({ internet: false });await sbx.files.write("/workspace/squares.lua", script);const syntax = await sbx.exec(["luac5.4", "-p", "squares.lua"]);if (syntax.exitCode !== 0) throw new Error(syntax.stderr);const run = await sbx.exec(["lua5.4", "squares.lua", "8"], { timeoutMs: 10_000 });console.log(run.exitCode, run.timedOut, run.stdout); // 0 false 1,4,9,16,25,36,49,64Pythonfrom withruntime import Sandboxscript = """local n = tonumber(arg[1])local squares = {}for i = 1, n do squares[#squares + 1] = i * i endprint(table.concat(squares, ","))"""with Sandbox.create(timeout_seconds=600, on_lease_end="stop") as sbx: sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q lua5.4", check=True, timeout_ms=300_000) sbx.network.set(internet=False) sbx.files.write("/workspace/squares.lua", script) syntax = sbx.exec(["luac5.4", "-p", "squares.lua"]) if syntax.exit_code != 0: raise SystemExit(syntax.stderr) run = sbx.exec(["lua5.4", "squares.lua", "8"], timeout_ms=10_000) print(run.exit_code, run.timed_out, run.stdout)Arguments arrive in Lua's arg table. Because the array form runs lua5.4
with no shell in between, an argument such as 8; rm -rf ~ stays one string
(run commands).
Many scripts, one sandbox or many?
Scripts from the same user can share a sandbox: write each to its own file and
run them in turn. Scripts from different users
should not share one, because a script can leave files behind for the next.
Start one sandbox per user or per job, stop it after, and let onLeaseEnd: "stop" clean up one you forget.
TypeScriptimport { Sandbox } from "withruntime";const jobs = ["print(1 + 1)", "print(('lua'):upper())", "while true do end"];await using sbx = await Sandbox.create({ image: "lua", network: { internet: false } });for (const [i, code] of jobs.entries()) { await sbx.files.write(`/workspace/job${i}.lua`, code); const run = await sbx.exec(["lua", `job${i}.lua`], { timeoutMs: 5_000 }); console.log(i, run.timedOut ? "timed out" : run.stdout.trim());}This uses lua, the name the image built below links to Lua 5.4; the third
job is stopped after five seconds and the loop carries on.
LuaRocks, then no network
luarocks install fetches rocks from the web and may compile C code with the
image's gcc. Install with the network on, then cut it:
Pythonfrom withruntime import Sandboxwith Sandbox.create(image="lua", timeout_seconds=900, on_lease_end="stop") as sbx: sbx.exec("sudo luarocks --lua-version=5.4 install dkjson", check=True, timeout_ms=300_000) sbx.network.set(internet=False) run = sbx.exec(["lua", "-e", 'print(require("dkjson").encode({ok = true}))']) print(run.stdout)Start every sandbox with Lua
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "lua", recipe: { apt: ["lua5.4", "liblua5.4-dev", "luarocks", "luajit"], commands: ["ln -sf /usr/bin/lua5.4 /usr/local/bin/lua"], },});await using sbx = await runtime.sandboxes.create({ image: "lua", network: { internet: false } });console.log((await sbx.exec(["lua", "-v"])).stdout);Building an image is free; a stored image is charged on its size, and the free trial stores your first three free (custom images). A sandbox waiting for its next script costs $0.03125 an hour at 2 vCPU and 4 GiB, because Runtime bills the CPU in use (pricing). New accounts get 50 free sandbox hours, no card.
See also running untrusted LLM code, turning off a sandbox's internet and how to run Bash scripts in a sandbox.
Sources
Checked 25 September 2026.
- Ubuntu 24.04 (noble) packages: lua5.4 and its file list, luajit, lua5.1, luarocks, liblua5.4-dev
- luac manual, Lua 5.4
Facts on this page were checked on 25 September 2026.