LuaJIT vs Lua: Compatibility, Performance, and OpenResty

API7.ai

September 23, 2022

OpenResty (NGINX + Lua)

LuaJIT and Lua are closely related, but they are not interchangeable names for the same runtime. Lua is the language and reference implementation; LuaJIT is a separate implementation optimized around a tracing just-in-time compiler and a foreign-function interface (FFI).

For OpenResty users, the practical answer is straightforward: use the OpenResty-maintained LuaJIT branch shipped and tested with your OpenResty release. For a standalone application, choose only after checking language compatibility, native-code requirements, portability, and the workload you will actually run.

LuaJIT vs Lua at a Glance

DimensionStandard LuaLuaJIT
Primary roleLanguage reference implementationPerformance-oriented Lua implementation
Language baselineDepends on the selected Lua releasePrimarily Lua 5.1 semantics with documented extensions
ExecutionBytecode virtual machineInterpreter plus tracing JIT compiler
Native integrationLua C APILua C API plus FFI
PortabilityBroad across supported platformsDepends on LuaJIT's supported CPU/OS combinations
OpenResty useNot the supported runtime for current OpenResty releasesOpenResty's maintained LuaJIT branch is the runtime

This table describes architecture, not a universal benchmark. LuaJIT can make hot, JIT-compatible code paths much faster, but startup, I/O, allocation, C calls, side exits, and code that the compiler cannot trace may dominate a real service.

What Is LuaJIT?

LuaJIT is an implementation of Lua with an interpreter and a tracing JIT compiler. Execution begins in the interpreter. When a path becomes hot, the runtime may record it as a trace, optimize it, and compile it to machine code. A guard failure can exit that trace and return execution to the interpreter or another trace.

That design has two important consequences:

  • JIT compilation is workload-dependent. A benchmark that repeatedly executes one numeric loop says little about a request path dominated by network I/O or allocations.
  • Code can be correct but still resist compilation. Unsupported operations, highly variable types, frequent guard failures, or excessive trace growth can keep work in the interpreter.

LuaJIT also provides an FFI library. FFI code can call compatible C functions and work with C data structures without writing a conventional Lua C module. This can reduce integration overhead, but it also crosses a safety boundary: incorrect declarations, object lifetimes, blocking C calls, and untrusted native libraries can crash or stall the process.

Language Compatibility: Start with Lua 5.1 Semantics

LuaJIT primarily implements Lua 5.1 language semantics and documents selected extensions and compatibility behavior. Do not assume code written for a later standard Lua release will run unchanged. Check the LuaJIT extensions, your dependencies, and your test suite before moving code between runtimes.

Common differences that deserve explicit tests include:

  • standard-library functions added, removed, or changed across Lua releases;
  • integer and number behavior expected by an application;
  • module loading and environment handling;
  • C modules compiled for a particular Lua ABI;
  • garbage-collection and finalizer assumptions;
  • platform-specific FFI declarations.

If portability across multiple Lua implementations is a requirement, define the supported language subset and run the same tests on every target runtime. A compatibility flag is not a substitute for that test matrix.

Why OpenResty Maintains Its Own LuaJIT Branch

OpenResty combines NGINX, LuaJIT, and lua-resty-* libraries to run Lua in NGINX request-processing phases. It maintains openresty/luajit2, a branch synchronized with upstream LuaJIT and supplemented with changes tested for the OpenResty ecosystem.

Use the runtime that comes with the target OpenResty release rather than replacing it with an arbitrary LuaJIT build. A different build can change patches, supported platforms, or behavior expected by lua-nginx-module and OpenResty libraries.

Each NGINX worker is a separate operating-system process. Workers may inherit initialized state when the master forks them, but they do not share one mutable Lua VM after the fork. Use supported inter-process mechanisms such as ngx.shared.DICT, an external data store, or message-based coordination for data that must be shared. Ordinary Lua tables and module globals remain worker-local.

For broader runtime context, see OpenResty vs NGINX and the guide to communication between OpenResty workers.

Performance: Measure the Whole Request Path

The useful question is not whether LuaJIT is "faster than Lua" in the abstract. It is whether the selected runtime meets latency, throughput, memory, startup, and operational requirements for a specific workload.

A defensible test should:

  1. use production-like code, data shapes, concurrency, and network dependencies;
  2. separate warm-up from steady-state measurements;
  3. report latency distributions, throughput, CPU, and memory together;
  4. exercise error paths and type variation, not only a hot loop;
  5. compare equivalent correctness and durability settings;
  6. pin the runtime, compiler options, CPU architecture, and operating system.

In OpenResty, blocking work is often more damaging than a small Lua optimization. A blocking filesystem, DNS, database, or native-library call can stall an event-loop worker. Prefer cosocket-based, nonblocking libraries in request phases, bound timeouts, and move blocking work outside the request path.

Code Patterns That Help

  • Declare variables and imported modules as local unless global state is intentional.
  • Reuse stable functions and data structures on hot paths where measurement supports it.
  • Keep value types predictable in performance-critical loops.
  • Avoid creating large temporary tables or strings without measuring allocation cost.
  • Inspect JIT behavior with development diagnostics before rewriting code around a guess.
  • Treat FFI as native code: review declarations, ownership, error handling, and blocking behavior.

These are starting points, not guarantees. Apply a change only when profiling shows that it improves the target workload without breaking correctness.

Lua Semantics That Commonly Cause Bugs

Tables Are Not Automatically Dense Arrays

Lua tables can represent arrays, maps, records, and mixed structures. The length operator is reliable for a sequence, where positive integer keys are contiguous from 1 to the boundary. For a table with holes, do not assume #table identifies the highest numeric key. Choose an explicit representation or track the length separately.

local dense = { "red", "blue", "green" } print(#dense) -- 3 local sparse = { [1] = "red", [3] = "green" } -- Do not use #sparse as a portable count of stored elements.

Undeclared Assignments Are Global

An assignment without local can write to the global environment. In a long-running worker, an accidental global can leak state between requests handled by that worker and make behavior difficult to reproduce.

local json = require("cjson.safe") local function decode(body) local value, err = json.decode(body) return value, err end

Use a linter and tests to catch accidental globals. Do not store request-specific identity, authorization, or mutable response data in module globals.

When to Choose Each Runtime

Choose the Lua runtime required and supported by the host platform first.

  • OpenResty or an OpenResty-based gateway: use the OpenResty-maintained LuaJIT branch shipped with the supported release.
  • Portable Lua application or embedded environment: standard Lua may be the simpler choice if the host and libraries target it.
  • Native integration or measured hot numeric paths: LuaJIT may help when its platform support and FFI risk are acceptable.
  • Application targeting newer standard Lua semantics: verify whether LuaJIT compatibility is sufficient; do not assume it.

FAQ

Is LuaJIT the same as Lua?

No. Lua is a language and reference implementation. LuaJIT is a separate implementation that primarily follows Lua 5.1 semantics and adds a tracing JIT compiler, FFI, and documented extensions.

Does LuaJIT always outperform standard Lua?

No. Results depend on the code path, data, warm-up, platform, and external I/O. Benchmark the complete workload and inspect whether important paths compile or frequently exit JIT traces.

Can OpenResty workers share Lua tables?

No. Workers are separate processes. Use ngx.shared.DICT or an external system for explicitly shared state, and design updates for concurrency and failure.

Should I install upstream LuaJIT over OpenResty's build?

Not by default. Use the OpenResty-maintained LuaJIT branch and version tested by your OpenResty distribution unless you have a validated compatibility and support plan.

Summary

LuaJIT is a performance-oriented Lua implementation, not a newer edition of the Lua language. Its tracing compiler and FFI can be valuable, but compatibility and performance must be verified on the actual workload. In OpenResty, the supported choice is the OpenResty-maintained LuaJIT branch, paired with nonblocking libraries, worker-safe state management, profiling, and version-pinned tests.

Share article link