# 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](/docs/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](/docs/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](https://www.lua.org/manual/5.4/luac.html)), then run it with its input passed as arguments. ```ts check import { Sandbox } from "withruntime"; const script = ` local n = tonumber(arg[1]) local squares = {} for i = 1, n do squares[#squares + 1] = i * i end print(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,64 ``` ```python check from withruntime import Sandbox script = """ local n = tonumber(arg[1]) local squares = {} for i = 1, n do squares[#squares + 1] = i * i end print(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](/docs/javascript#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. ```ts check import { 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: ```python check from withruntime import Sandbox with 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 ```ts check import { 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](/docs/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](/docs/pricing)). New accounts get 50 free sandbox hours, no card. See also [running untrusted LLM code](/use-cases/run-untrusted-llm-code), [turning off a sandbox's internet](/how-to/turn-off-sandbox-internet) and [how to run Bash scripts in a sandbox](/languages/bash). ## Sources Checked 25 September 2026. - Ubuntu 24.04 (noble) packages: [lua5.4](https://packages.ubuntu.com/noble/lua5.4) and its [file list](https://packages.ubuntu.com/noble/amd64/lua5.4/filelist), [luajit](https://packages.ubuntu.com/noble/luajit), [lua5.1](https://packages.ubuntu.com/noble/lua5.1), [luarocks](https://packages.ubuntu.com/noble/luarocks), [liblua5.4-dev](https://packages.ubuntu.com/noble/liblua5.4-dev) - [luac manual, Lua 5.4](https://www.lua.org/manual/5.4/luac.html) Facts on this page were checked on 25 September 2026.