Lua
First appeared 1993 · Roberto Ierusalimschy
A few hundred kilobytes of scripting language, embedded in more software than you realise.
Overview
Lua is a small, fast scripting language designed from the beginning to be embedded inside other programs. The entire reference implementation is around thirty thousand lines of clean ANSI C and compiles to a few hundred kilobytes, which is why it appears anywhere a host application needs user-facing scripting without a heavy runtime: game engines, network appliances, editors, embedded devices and databases. The language itself is deliberately minimal, built on a single composite data structure — the table — that serves as array, dictionary, object and namespace. Metatables provide a small, powerful mechanism for operator overloading and inheritance, letting Lua support object-oriented and functional styles without building either into the core. LuaJIT, an independent just-in-time implementation, reaches performance competitive with C for many workloads and is widely used where speed matters.
Key facts
The reference details, without the paragraph.
- First appeared
- 1993
- Designed by
- Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes, PUC-Rio
- Typing
- Dynamic and strong, with eight basic types and one composite type
- Execution
- Compiled to bytecode for a register-based virtual machine; LuaJIT adds a tracing JIT
- Memory model
- Automatic — incremental or generational garbage collection
- Package manager
- LuaRocks
- File extensions
- .lua
- Implementation size
- Roughly 30,000 lines of ANSI C; the binary is a few hundred kilobytes
- Indexing
- One-based, and `nil` marks the end of a sequence
- Licence
- MIT
History
How the language got here — the decisions that still shape how you write it.
Lua was created in 1993 at the Pontifical Catholic University of Rio de Janeiro by Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes. Its origin is unusually specific: Brazil had strict trade barriers on imported computer hardware and software through the 1980s, so the university's research group built its own tools rather than buying them. Lua — Portuguese for moon — grew out of two earlier in-house languages for configuring engineering applications at Petrobras, the Brazilian oil company. That heritage explains its defining characteristics: it had to be tiny, portable to whatever hardware was available, and easy to embed in existing C programs. The design proved unusually well suited to games, where a compiled engine needs designers and modders to script behaviour without recompiling. World of Warcraft, Roblox, Garry's Mod and countless others adopted it. Later, Neovim chose Lua as its configuration and plugin language, Redis embedded it for server-side scripting, and OpenResty built a high-performance web platform on LuaJIT inside nginx. Few languages are so widely deployed while being so rarely written as a primary language.
- 1993
Born from a trade embargo
Brazil's restrictions on imported software through the 1980s meant PUC-Rio's research group built its own tools. Lua grew from two in-house configuration languages written for Petrobras.
- 1996
Lua 3.0 and international attention
An article in Dr. Dobb's Journal introduces Lua to a wider audience, and game developers start noticing a scripting language small enough to embed anywhere.
- 2003
Games adopt it broadly
Lua becomes the default embedded scripting language in the games industry — a compiled engine with designer-editable behaviour is exactly the problem it solves.
- 2004
World of Warcraft ships
Its entire addon and interface system is Lua, exposing the language to an enormous number of people who were not otherwise programmers.
- 2005
LuaJIT
Mike Pall's tracing just-in-time compiler reaches performance competitive with C on many workloads, and becomes the implementation of choice where speed matters.
- 2015
Lua 5.3 adds integers
A distinct integer subtype arrives after two decades of all numbers being doubles — a small change with wide consequences for correctness in embedded and financial code.
- 2021–2025
Neovim and Luau
Neovim adopts Lua as its configuration and plugin language, and Roblox ships Luau, a gradually-typed dialect, to millions of young developers.
What it is good at
The reasons teams pick it, stated concretely.
Small enough to embed anywhere
The whole interpreter compiles to a few hundred kilobytes of ANSI C with no dependencies. That is why it fits in game engines, routers, cameras and microcontrollers where a Python runtime would be unthinkable.
A genuinely clean C API
The stack-based interface for exchanging values between C and Lua is small, well documented and stable across versions. Embedding it in an existing C program is a day's work, not a project.
One data structure that does everything
Tables serve as arrays, dictionaries, objects, modules and namespaces. Learning one structure well covers most of the language, which is a large part of why Lua is quick to pick up.
LuaJIT is remarkably fast
For numeric and loop-heavy code, LuaJIT frequently reaches within a small factor of C. Its FFI also calls C functions directly with no binding layer, which is why OpenResty and similar systems chose it.
Easy for non-programmers to reach
The syntax is small and forgiving, which is why it succeeded as a modding and configuration language. Millions of people have written Lua without considering themselves developers.
Trade-offs
Every language costs you something. Knowing what, before you commit, is the whole point.
A very small standard library
There is no built-in HTTP, JSON, filesystem traversal or date parsing beyond the basics. This is deliberate — the host application is expected to provide what it needs — but standalone Lua means assembling libraries yourself.
One-based indexing and nil-terminated sequences
Arrays start at 1, and a nil in the middle of a table makes its length undefined. Both surprise people constantly and are a genuine source of bugs when moving between languages.
Versions are not compatible
5.1, 5.2, 5.3 and 5.4 each broke something, and LuaJIT is pinned to 5.1 semantics. A library often targets one specific version, and the ecosystem is fragmented as a result.
Globals by default
An undeclared variable is global, so a typo silently creates one rather than raising an error. `local` must be written deliberately and consistently.
No standard object system
Object orientation is built by hand from tables and metatables, so every codebase and framework does it slightly differently. There is no single conventional class pattern to learn.
Code examples
Not syntax tours — the idioms that make code read like the language rather than a translation of another one.
-- Array-like: indices start at 1.
local books = { "Dune", "Neuromancer", "Snow Crash" }
print(#books, books[1]) -- 3 Dune
-- Dictionary-like, in the same structure.
local book = {
title = "Dune",
year = 1965,
tags = { "scifi", "classic" },
}
-- Both notations reach the same field.
print(book.title, book["title"])
-- ipairs stops at the first nil; pairs visits every key in no order.
for i, title in ipairs(books) do print(i, title) end
for key, value in pairs(book) do print(key, value) end
-- A nil in the middle makes # undefined — do not rely on it.
books[2] = nil
print(#books) -- may be 1 or 3local Account = {}
Account.__index = Account -- lookups fall back to Account
function Account.new(owner, balance)
return setmetatable({ owner = owner, balance = balance or 0 }, Account)
end
function Account:deposit(amount) -- `:` passes self implicitly
assert(amount > 0, "amount must be positive")
self.balance = self.balance + amount
return self
end
function Account:__tostring()
return string.format("%s: %.2f", self.owner, self.balance)
end
local acct = Account.new("Ada", 100)
acct:deposit(50):deposit(25)
print(tostring(acct)) -- Ada: 175.00-- Functions return multiple values natively.
local function divide(a, b)
if b == 0 then return nil, "division by zero" end
return a / b, nil
end
local result, err = divide(10, 0)
if err then print("error: " .. err) end
-- Closures capture upvalues.
local function counter()
local count = 0
return function()
count = count + 1
return count
end
end
local next_id = counter()
print(next_id(), next_id()) -- 1 2
-- Varargs
local function log(level, fmt, ...)
print(string.format("[%s] " .. fmt, level, ...))
end#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
static int l_add(lua_State *L) {
double a = luaL_checknumber(L, 1); /* validates and errors cleanly */
double b = luaL_checknumber(L, 2);
lua_pushnumber(L, a + b);
return 1; /* number of return values */
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushcfunction(L, l_add);
lua_setglobal(L, "add");
if (luaL_dostring(L, "print(add(2, 3))")) {
fprintf(stderr, "%s\n", lua_tostring(L, -1));
}
lua_close(L);
return 0;
}Common pitfalls
The mistakes that cost everyone an afternoon at least once.
Forgetting `local`
An undeclared variable is global. A typo creates a new global rather than erroring, and a loop variable without `local` leaks and can collide across modules. Run luacheck.
Mixing `.` and `:` on methods
`obj.method(x)` passes x as self, while `obj:method(x)` passes obj as self and x as the first argument. The resulting errors are confusing because nothing looks wrong at the call site.
Relying on `#` with holes
The length operator is only defined for sequences with no nil gaps. With holes it may return any boundary — track the count explicitly instead.
Assuming pairs iterates in order
`pairs` visits keys in unspecified order that can change between runs. Use `ipairs` for sequences, or collect and sort the keys when order matters.
String concatenation in a loop
Strings are immutable and interned, so `s = s .. x` in a loop is quadratic. Collect into a table and call `table.concat` once.
Version mismatches
A rock built for 5.1 will not necessarily work on 5.4, and LuaJIT follows 5.1. Check what your host embeds before choosing libraries.
In production
Where it is running at scale, and what it is doing there.
Roblox
Luau, a Lua dialect, is the scripting language for every experience on the platform.
Blizzard
World of Warcraft's entire addon and user interface system is scripted in Lua.
Cloudflare
OpenResty and LuaJIT power request processing at the edge.
Neovim
Lua is the configuration and plugin language for the editor.
Learning path
A realistic order to learn things in, with something to build at each step.
- 1
Days 1–3
The whole language
Lua is small enough to cover in a few days: values and types, tables, functions, closures, control flow, and `local` versus global. Read the reference manual — it is unusually short and readable.
Build this: Write a script that reads a text file and reports word frequencies using a table.
- 2
Week 1
Metatables and idiom
Metatables, `__index`, the colon syntax, multiple returns, varargs, and `pcall` for error handling. Metatables are the mechanism behind objects, operator overloading and most library magic.
Build this: Implement a small class hierarchy with inheritance using metatables.
- 3
Week 2
The environment you will actually use
Lua is nearly always embedded, so learn the host: Neovim's API, Roblox's Luau, Love2D, or the C API if you are embedding it yourself. The host's library is most of what you will call.
Build this: Write a Neovim plugin or a small Love2D game, depending on where you are heading.
- 4
Weeks 3–4
Tooling and correctness
LuaRocks, busted for tests, luacheck for static analysis — luacheck in particular catches accidental globals, which the language will not. Learn coroutines for cooperative concurrency.
Build this: Add luacheck and a test suite to your project and fix everything it reports.
- 5
Ongoing
Performance and integration
LuaJIT and its FFI, garbage collector tuning, profiling, and the details of the C API if you are embedding. Understand which version your host uses — it constrains everything.
Build this: Port a hot path to LuaJIT's FFI and measure the difference.
Ecosystem and tooling
The tools you will end up installing whichever project you join.
| Tool | Category | What it does |
|---|---|---|
| LuaJIT | Runtime | Tracing JIT compiler with a C FFI; near-C performance, pinned to Lua 5.1 semantics |
| LuaRocks | Packaging | The package manager and module repository |
| luacheck | Code quality | Static analysis — catches accidental globals, unused variables and shadowing |
| busted | Testing | The standard BDD-style testing framework |
| Neovim | Host | Uses Lua for configuration and plugins, with a large and active plugin ecosystem |
| Love2D | Games | A 2D game framework where Lua is the primary language rather than an embedded one |
| OpenResty | Web | nginx with LuaJIT embedded, for high-performance request processing at the edge |
| Luau | Dialect | Roblox's gradually-typed, sandboxed Lua dialect, now open source |
Lua libraries
Library coverage for Lua is on the way.
The guide above is complete. In the meantime, the catalogues for Python, Java, JavaScript, C and C++ are fully written.
Browse all librariesFrequently asked
Why does Lua index from 1?
It follows the mathematical and Fortran convention, and the authors considered it more natural for the engineers who were its original audience. It is a genuine source of off-by-one errors when moving between languages, and it will not change — too much code depends on it.
Should I use Lua or LuaJIT?
LuaJIT if you need speed or the C FFI, and your host allows it — for numeric and loop-heavy code the difference is large. Standard Lua if you need 5.2 or later features, since LuaJIT is frozen at 5.1 semantics with some backports. In practice the host application usually decides for you.
Is Lua worth learning if I am not doing games or Neovim?
As a general-purpose language, probably not — Python covers that ground far better. Learn it when your host uses it: a game engine, Redis scripting, nginx via OpenResty, a network appliance, or embedded firmware. Its value is being present where nothing else fits.
How does Lua handle errors?
Two ways. The convention for expected failures is returning `nil, message`, checked by the caller — as in Go. For genuine exceptions, `error()` raises and `pcall`/`xpcall` catch. Mixing the two arbitrarily makes code hard to follow, so pick a convention per module.
What are coroutines actually for?
Cooperative multitasking on a single thread — a coroutine yields control explicitly rather than being preempted. They are excellent for game state machines, generators and iterative algorithms that need to pause. They are not parallelism: only one runs at a time.
How do I do object-oriented programming in Lua?
By hand, with tables and metatables — there is no built-in class system. The common pattern is a table with `__index` pointing at itself, plus `setmetatable` in a constructor. Because every framework does this slightly differently, read the host's convention before inventing your own.




