Vivian Voss

Lua Tables: The Configuration Format That Admits What It Is

lua freebsd tooling architecture

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.

Pick the Smallest Expressiveness That Fits .env key = value JSON pure data, no comments TOML hier. data + comments Lua tables data + opt-in logic a real program when you must expressiveness

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.

Where Lua Runs, Quietly, in 2026 Embedded FreeBSD loader.lua routers, set-top, IoT Editors Neovim init.lua Awesome rc.lua Web edge OpenResty, Kong, HAProxy, Cloudflare Games WoW, Roblox/Luau Civ V/VI, Garry's Mod Document pandoc filters LuaTeX, Lightroom Diagnostics Wireshark dissectors Solaris DTrace Database Redis EVAL since 2012 Thirty-three years, unrelated communities, the same trade made repeatedly.

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.

Same Data, Two Type Tables YAML 1.1 Lua countries: - DE - FR - NO countries = { "DE", "FR", "NO" } parses as ["DE","FR", false ] Norway silently removed YAML 1.1 implicit boolean ["DE","FR","NO"] three strings, exactly three no implicit type table

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 .env file.
  • 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.lua becomes the default boot loader; FORTH removed in FreeBSD 13. Neovim 0.5 (July 2021): init.lua is 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.