Skip to content
Game Development8 min read

Lua is not a toy language, and pretending it is costs you money

It is the embedded scripting layer behind an enormous amount of software you already use. Here is what we have learned building production systems on it, and where it genuinely will hurt you.

Simbarashe Maunga
Founder & Lead Engineer

Every few months someone asks us, politely, why a serious engineering firm lists Lua next to Java and Rust. The assumption underneath is that Lua is what you use for a game jam, or a config file that wanted to be more.

Lua runs a meaningful share of the game industry's gameplay logic, the plugin layer of Redis, the scripting inside Nginx and Kong, the automation surface in a long list of network appliances, and the mod ecosystems of games that have outlived entire console generations. It is a small, sharp, deeply thought-through language that happens to be very easy to underestimate.

What it is actually for

Lua is not really a general-purpose application language and it is not trying to be. It is an embedding language. Its reason to exist is to be dropped inside a host program written in something faster and lower-level, and to expose a safe, fast, hot-reloadable scripting surface to people who should not be recompiling the engine.

Once you see it that way the design choices stop looking odd:

  • The whole interpreter is a few hundred kilobytes. You can embed it in something small.
  • There is one data structure, the table, that serves as array, hash map, object, namespace, and module. Learning the language takes an afternoon.
  • Coroutines are built in, and they are true coroutines with symmetric transfer, not sugar over a thread pool. For gameplay logic (a cutscene, an enemy behaviour that waits three seconds and then charges, a dialogue tree), this is a far more natural fit than callbacks or a state machine.
  • The C API is genuinely small enough to hold in your head.

The yield-based control flow deserves special mention. Writing "walk to the door, wait for it to open, walk through, then say the line" as straight-line code that suspends is enormously clearer than the equivalent state machine. That readability is the reason designers who are not engineers can be trusted with the file.

Where it will bite you

We would be doing nobody a favour by pretending it is all upside. Three things reliably cause pain, and all three are manageable if you know about them on day one.

Global by default. A bare assignment creates a global. One typo in a variable name silently writes into the global table and the bug surfaces forty minutes later in a completely unrelated system. This is not a matter of discipline; it is a matter of tooling. Run luacheck in CI from the first commit, treat globals as errors, and the entire class of bug disappears. On projects that came to us without it, this is consistently the largest single source of ghost bugs.

One-based indexing and nil semantics. Arrays start at one. The length operator on a table containing a nil in the middle is formally undefined: it may return the index before the hole or after it, and you do not get to rely on which. If you are storing a sparse collection, do not use the length operator; track the count yourself. Every team learns this once, usually via a loop that silently stops halfway.

The garbage collector runs when it wants to. In a game loop this is the difference between a smooth frame and a visible hitch. The fix is not to fight the collector, it is to stop generating garbage in the hot path: preallocate and reuse tables rather than constructing them per frame, avoid creating closures inside update, and keep string concatenation out of anything that runs sixty times a second. In the worst cases you step the collector manually during a scene transition where a pause is invisible anyway.

The architectural decision that actually matters

The important question on a Lua project is almost never a Lua question. It is: where exactly is the line between the host and the script?

Get it wrong in one direction and you have written your physics in an interpreted language and it is too slow. Get it wrong in the other and every designer tweak needs an engineer, a recompile, and a deploy, which defeats the entire purpose of embedding a scripting language.

The line we keep coming back to: anything that runs per-entity per-frame belongs in the host. Anything that expresses a decision belongs in script: when the boss enrages, what the quest requires, how the reward scales, what the AI does when it loses sight of you. Host code answers "how"; script answers "what" and "when".

That boundary has a pleasant side effect. It makes the scripting layer the specification. When the designer changes the file, the design has changed, and there is no second document to fall out of date.

Why we are still recommending it

Because the alternative for the same job is usually a bespoke configuration format that slowly grows conditionals, then variables, then loops, then a badly specified expression evaluator, and arrives after two years at a worse language than Lua with none of the tooling and one implementer who has since left.

If the requirement is "non-engineers need to change behaviour safely without a recompile", that problem was solved decades ago. Use the solution.

luagame-devscriptingarchitecture
Let's talk

Got a problem like this one?

We'd rather look at your actual system than guess. Tell us what you're dealing with.