By Design ■ Episode 06
In 1993, three engineers at PUC-Rio's Tecgraf laboratory had a problem. Petrobras, the Brazilian oil company, ran software on every kind of machine it owned, and the configuration files for that software were a mess. The team had two existing prototypes: DEL, a "data-entry language", and Sol, a "simple object language" used as a configuration format. Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes met in mid-1993 and concluded that the two languages could be replaced by a single, more powerful one. They named the result Lua: "moon" in Portuguese, because Sol meant "sun" and the joke was difficult to resist.
Lua was not born as a scripting language. It was born as a configuration language. The scripting came later, and the table syntax that made the scripting language pleasant was the same table syntax that had been designed for configuration in the first place.
That is the entire foundation of the argument that follows.
The Complaint
The standard objection, voiced often since 2003 and still occasionally in 2026, is that programming languages are unsafe as configuration formats. They allow arbitrary code execution. They allow infinite loops, which on a boot path will lock you out of your own machine. They allow side effects (file reads, network calls, time-dependent values) that make configuration files non-reproducible. They cannot be validated by external tools because the validator would have to run the code. Configuration, the argument goes, must be declarative data, not code.
The argument is not entirely wrong. The conclusion that follows from it (reach for YAML) is.
The Decision
Lua has exactly one compound data structure: the table. A
table is, simultaneously, an array, a hash map, a record,
an object, and a namespace. The same { ... }
construct does all five jobs, and any one table can mix
them.
A configuration file in Lua looks like this:
opts = {
prompt = "$ ",
history = 1000,
plugins = { "git", "ssh", "fzf" },
bindings = {
["ctrl-r"] = "history-search",
["ctrl-l"] = "clear-screen",
},
}
Three observations make this scheme remarkable.
First, the file is data. The shape on the page is the shape
in memory after loading. There is no parser layer between
what the user wrote and what the program reads.
dofile("config.lua") returns a table, and the
table is the configuration. Loading needs no library
because the language already loaded itself.
Second, the file is also code, but only when you ask it to be. The same syntax that holds static values can also hold variables, conditionals and function calls:
local hostname = os.getenv("HOSTNAME") or "unknown"
opts = {
prompt = hostname == "production" and "PROD$ " or "$ ",
log = "/var/log/" .. hostname .. ".log",
}
The default form is declarative; the expressive form is opt-in. You pay for the expressiveness only when you reach for it. A Lua config that contains no functions, no loops and no conditionals is, for all practical purposes, indistinguishable from JSON. With trailing commas. And comments.
Third, the format scales without changing format. A twenty-line declarative table for a small tool, a three-hundred-line nested configuration for a window manager, a three-thousand-line plugin manifest for an editor: all the same syntax, all the same loading mechanism, all the same mental model. There is no point at which "this configuration grew too complex for YAML, we need to migrate to a programming language". The language is already there.
These three properties cost the format the elegance of being purely declarative. They bought the format everything else.
The Trade-Off
The honest costs are real.
Turing-completeness is a footgun. A
configuration mistake can lock the boot loader in an
infinite loop. A misplaced while true do in
loader.lua will keep your machine off until
you boot from rescue media. YAML cannot do this. JSON
cannot do this. The price of having an escape hatch is
that the escape hatch can be misused.
The interpreter is not free. A statically linked Lua 5.4 interpreter is around 270 kilobytes. For an embedded system or a single-purpose CLI tool that already wanted to read a YAML file, that is a real binary-size cost. For anything larger than a router firmware, it is rounding error.
Schema validation is less mature. JSON Schema has an ecosystem: code generators, IDE integration, OpenAPI tooling, automated test fixtures. Lua-side equivalents exist (LuaCheck, the Teal language, Penlight assertions) but they are not universal. If your stack already relies on JSON Schema for cross-team contracts, Lua is a step back.
Users must learn the format. A user editing
init.lua for the first time has to recognise
that a missing comma is a syntax error and that
local does not appear in JSON or YAML. The
learning curve is small but real, and projects that target
non-programmer end users (system administrators, content
editors, hobbyists) often choose YAML for that reason.
In return for those four costs, you get a format that admits what it is, refuses to coerce strings into booleans behind your back, scales from twenty lines to three thousand without changing format, and has comments that work.
That is, on balance, a rather good deal.
The Proof
The list of projects that have made this exact trade is long, and the projects are unrelated to one another, and the choice keeps being made.
FreeBSD's boot loader. Lua entered FreeBSD
as a 2014 GSoC project mentored by Wojciech Koszek. It
became the
default
boot loader in FreeBSD 12 (December 2018), shipping
alongside the older FORTH-based loader as a fallback. In
FreeBSD
13 (April 2021), FORTH was removed entirely. The file
/boot/lua/loader.lua is what reads
/boot/loader.conf, draws the boot menu,
decides between kernels, and hands control to whatever you
boot. That is rather a lot of trust to place in a
"configuration language".
Neovim. The fork of Vim that started in
2014 made Lua a first-class scripting and configuration
language from the start.
Neovim
0.5 (July 2021) made init.lua the
recommended configuration entry point, supplanting
Vimscript. The plugin ecosystem followed within eighteen
months. The Vim community is, generally speaking, the
opposite of an early-adopter culture, and they switched.
Awesome Window Manager.
rc.lua
has been the configuration file since version 3.0 in
2008. The window manager is itself a Lua interpreter with
X11 bindings.
Wireshark. Protocol dissectors are written in Lua. The reasoning, documented in the Wireshark wiki, is that a network analyst diagnosing an unknown protocol at 02:00 should not have to write C and recompile.
OpenResty / nginx. Cloudflare's edge stack runs on OpenResty, an nginx variant with embedded Lua for request-handling logic and configuration. Roughly twenty per cent of all web traffic touches it.
HAProxy. Lua scripting since version 1.6 in 2015. Used for custom routing logic that exceeds what the configuration language can express on its own.
Adobe Lightroom. Plugin scripting and large parts of Lightroom's own user interface run on Lua.
Redis.
Server-side
scripting via EVAL since version 2.6 in
2012.
World of Warcraft. The
entire
user-interface customisation system, from launch in
2004, runs on Lua. There is a generation of programmers
whose first encounter with the language was a
FrameXML.toc file.
Roblox. A Lua dialect (Luau) is the scripting and configuration language for tens of millions of daily players' games and game logic.
Kong API Gateway. Configuration plugins are Lua modules.
pandoc. Document filters and writers are Lua scripts.
Conky, awesome, mpv, VLC, Vifm, Premake, Torch (the original deep-learning library), TeX Live's LuaTeX, NetBSD's ATF testing framework, Cisco IOS, Solaris's DTrace front-end, the BBC iPlayer set-top boxes. The list is genuinely long. It is also genuinely heterogeneous. These are not projects that share a community, a language family or a problem domain.
What they share is the same trade. They wanted configuration files that scaled into structured logic when needed, that did not lie about their type system, and that carried no parser dependency. They picked Lua tables because Lua tables are the cheapest format that meets all three criteria.
A Footnote on the Norway Problem
YAML's most-cited footgun deserves a closer look, because it illustrates the cost of a configuration format that pretends to be data while behaving like a language.
YAML 1.1,
the most widely deployed version of the specification,
recognises the following strings as boolean literals
(case-insensitive): y, Y,
yes, Yes, YES,
n, N, no,
No, NO, true,
True, TRUE, false,
False, FALSE, on,
On, ON, off,
Off, OFF. The complete list runs
to about thirty entries.
The Norway problem is what happens when a configuration file contains a list of country codes:
countries:
- DE
- FR
- NO
A YAML 1.1 parser reads NO as the boolean
false. If your application expects strings
and you have not explicitly quoted the entry, "Norway"
silently disappears from your country list. There is a
class of production bug,
well-documented
across many companies, where customers in Norway found
themselves locked out of services because some
configuration pipeline had passed through a YAML 1.1
parser.
YAML 1.2
(2009) narrowed the boolean grammar to true,
True, TRUE, false,
False, FALSE. However, the most
widely deployed YAML libraries, including the default load
mode of Python's PyYAML, continue to use 1.1 semantics for
backward compatibility. You can opt out, but you have to
know to.
The same line written as a Lua table:
countries = { "DE", "FR", "NO" }
is, unambiguously, a sequence of three strings. The language
has no opinion about your country codes. It has no opinion
about your version codes either: version = "1.10"
is a string of length four, version = 1.10 is
a number. The user picked which they wrote and the
language honoured the choice. There is no implicit type
table to know.
This is what "admits what it is" means in practice. A format that lies to you about its types saves you keystrokes today and costs you incident hours later.
The Principle
Configuration is code, even when it pretends not to be.
YAML is a Turing-complete configuration language with
anchors (&), aliases (*),
merge keys (<<), type coercion, custom
tags, and a specification long enough that real-world
parsers ship informal subsets of it. It is a programming
language in the costume of a markup format. The cost of
the costume is paid in subtle bugs that surface months
after the configuration was written and do not look like
programming bugs because the configuration "is just YAML".
Lua is a programming language honest enough to admit what it is. The configuration table is just data when you treat it as data, and becomes structured logic only at the points where you reach for the escape hatch. The expressiveness is opt-in, not opt-out. The format does not pretend to be safer than it is.
There is a second-order point worth making. Format choice signals intent. A team that picks YAML is signalling that the configuration is data and the Turing-complete behaviour is incidental. The cost of that signal is the recurring small surprise that the language does not actually behave like data. A team that picks Lua is signalling that the configuration is, fundamentally, code in a constrained style. The team is more likely to write tests for it, reviewers are more likely to read it like code, and the surprise budget is spent up front instead of on a Tuesday in July.
The general lesson goes beyond Lua. Pick the smallest amount of expressiveness that the configuration actually requires:
- Static key/value pairs, no nesting: a
.envfile. - Pure data, no logic, no comments: JSON.
- Hierarchical pure data with comments and ordering: TOML.
- Structured data with optional logic, escape hatch when the configuration grows: Lua tables.
- Anything beyond that: write a real program.
Skipping levels is what creates the "well, we need to template our YAML now" moment, which is the moment a project realises it picked the wrong level of expressiveness six months ago. The fix is rarely more YAML.
Closing
Three engineers in Rio de Janeiro picked the right level of expressiveness in 1993, for a Brazilian oil company that needed configuration files that scaled. The format they designed has since spent thirty-three years quietly becoming the configuration language of choice for projects that, for one reason or another, found themselves wanting more than YAML and less than Python.
The escape hatch is opt-in. The default form is declarative. The Norway problem cannot occur. Comments work. Trailing commas work. Indentation does not matter.
The honest format is the one that does not lie about being a language.
1993, PUC-Rio Tecgraf: Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes merge DEL and Sol into Lua for Petrobras. Lua's table is array, hash, record, object and namespace in one construct. Default declarative; expressive form opt-in. FreeBSD 12 (December 2018):
/boot/lua/loader.luabecomes the default boot loader; FORTH removed in FreeBSD 13. Neovim 0.5 (July 2021):init.luais the recommended config. OpenResty runs Cloudflare's edge. Awesome WM, Wireshark, World of Warcraft (2004), Roblox, Kong, HAProxy, Lightroom, Redis EVAL, pandoc. The Norway problem cannot occur because Lua has no opinion about your country codes. The honest format is the one that admits what it is.