LuaJIT in OpenResty: How It Works and How luajit2 Differs
OpenResty runs Lua code on LuaJIT 2.1, embedded in each Nginx worker process, where request code runs in lightweight coroutines and hot paths are compiled to machine code. Open-source releases bundle the OpenResty-maintained luajit2 branch with the other components of each release. In short, LuaJIT is the runtime, while luajit2 is OpenResty’s maintained branch of LuaJIT with additional changes for OpenResty; its README describes it as a branch rather than a fork because it regularly synchronizes upstream changes. OpenResty Inc. also provides LuaJIT-plus, a private enhanced runtime that can directly replace the standard LuaJIT runtime. This article explains how LuaJIT in OpenResty works and what the differences mean in practice.
Which Lua Runtime Does OpenResty Use?
LuaJIT is an interpreter and just-in-time (JIT) compiler for the Lua language, created by Mike Pall. It implements Lua 5.1, provides some Lua 5.2 compatibility extensions, and includes LuaJIT-specific features such as ffi, bit, and jit. OpenResty uses LuaJIT 2.1; since lua-nginx-module v0.10.16, the module has required LuaJIT and no longer supports the standard Lua interpreter (PUC-Rio Lua).
OpenResty releases use the luajit2 branch maintained by OpenResty. The branch periodically synchronizes upstream changes and also contains modifications maintained by OpenResty.
The LuaJIT version bundled with each OpenResty release can be confirmed from the corresponding release notes and source package. For example, OpenResty 1.31.1.1 bundles LuaJIT v2.1-20260415. Deployments should generally use the combination bundled with the release instead of assembling LuaJIT, lua-nginx-module, and other dependencies independently.
How Lua Code Runs Inside Nginx Workers
Workers, Virtual Machines, and Module State
The LuaJIT virtual machine runs inside an Nginx worker process. The HTTP and Stream subsystems each use a Lua VM; whether a worker has one or two VMs depends on which Lua subsystems it actually enables. Different worker processes do not share Lua VMs.
Modules loaded with require are cached in the corresponding VM. Module-level variables and the state they reference remain available to subsequent requests handled by that worker, while other workers have their own module instances and state. Data that must be shared across workers should use mechanisms such as lua_shared_dict; see Sharing Data Across Requests in OpenResty.
Request Phases and Coroutines
Nginx divides request processing into phases such as rewrite, access, content, and log. OpenResty runs Lua code in the corresponding phases through directives such as rewrite_by_lua*, access_by_lua*, and content_by_lua*.
Lua code in request processing runs in lightweight Lua coroutines. When a cosocket accesses Redis, MySQL, or an HTTP upstream, the current coroutine is suspended while waiting for network I/O, allowing the worker to process other events. The coroutine resumes when data is ready. This behavior depends on the nonblocking APIs provided by OpenResty; blocking calls in the standard Lua libraries do not automatically become nonblocking. Readers new to OpenResty can follow the seven-step hands-on tutorial for the complete application flow.
From Interpretation to JIT Compilation
LuaJIT initially executes code with its interpreter while monitoring hot paths. Once compilation conditions are met, the JIT records an execution path and attempts to generate machine code, called a trace. Later executions that satisfy the conditions for that trace can run the generated machine code.
A trace abort can have several causes, such as encountering an operation without JIT support (NYI) or reaching a recording limit. It means that one trace recording was stopped; it does not mean that the entire function can only be interpreted from then on. Later executions may continue in the interpreter or trigger another compilation attempt. Abort counts, lj_BC_* frames, and trace#N frames are therefore diagnostic clues that should be interpreted together with sampling data and the relevant analysis tools.
Benchmarks should also account for JIT warm-up. If the goal is to measure steady-state hot-path performance, run the code until it reaches the expected state before timing it. See How to Measure Lua Code Execution Time Correctly in OpenResty. For how traces and interpreted execution appear in sampling flame graphs, see Lua CPU Flame Graphs in Practice; for inspecting JIT logs during development, see the resty Command-Line Tool demonstration.
How Does luajit2 Differ from Upstream LuaJIT?
The branch changes over time. The following are representative changes documented in the luajit2 README; the exact features and parameters should be checked against the LuaJIT version and build options in use.
Table Operation APIs
luajit2 provides several table operation functions, including table.isempty, table.isarray, table.nkeys, and table.clone. They can be used to test whether a table is empty, determine whether it has an array shape, count its keys, and make a shallow copy. The branch documentation says these APIs can be JIT-compiled. Check the documentation for the relevant version, especially the definitions of table.isarray for empty and sparse tables.
JIT Defaults for Large Applications
luajit2 uses default values for some JIT parameters that are better suited to large OpenResty applications. The branch README lists the following settings:
maxtrace=8000
maxrecord=16000
minstitch=3
maxmcode=40960 -- KB
maxtrace controls the maximum number of traces in the trace cache, maxrecord limits the number of IR instructions that a single trace can record, minstitch is the minimum number of IR instructions required to stitch traces, and maxmcode limits machine-code memory. For comparison, the upstream LuaJIT documentation lists defaults of 1000 for maxtrace, 4000 for maxrecord, and 2048 KB for maxmcode. Parameters can vary by branch version and runtime settings; use jit.opt or the relevant diagnostic tools to confirm the actual values in a process.
C APIs Used by OpenResty Core
The branch adds C APIs used by the OpenResty core. For example, lua_resetthread resets a Lua thread for reuse, and lua-nginx-module’s lua_thread_cache_max_entries uses this capability to cache lightweight threads. The branch also defines the OPENRESTY_LUAJIT macro in luajit.h, allowing C modules to identify OpenResty-specific versions at compile time.
These interfaces are mainly intended for the OpenResty core and C module developers. When using upstream LuaJIT or assembling dependencies independently, check the specific version instead of assuming that all branch features are interchangeable. lua-nginx-module still supports official LuaJIT 2.x releases, but its documentation notes that performance will be significantly lower, and Nginx logs an alert when it detects a LuaJIT build that is not OpenResty’s.
Bytecode Tools and Other Changes
The luajit2 bytecode dump tool adds the -bL option for displaying source line numbers and updates the -bl output. The branch also raises the maximum number of allowed upvalues and contains JIT and VM fixes, as well as debugging aids, that have not yet been merged upstream. See the luajit2 README for the complete list and applicable versions.
GC64 is another feature that is easy to confuse with branch changes: it is a LuaJIT build mode. Since OpenResty 1.15.8.1, OpenResty has built with GC64 enabled by default on x86_64; the mode raises the address-space limit that LuaJIT’s GC can manage. See LuaJIT GC64 Mode for background and limitations.
LuaJIT-plus: A Runtime Enhancement for Production Memory Management
The section above describes the luajit2 bundled by default in open-source OpenResty releases. For production services that need tighter control over long-running memory usage, OpenResty Inc. also provides LuaJIT-plus. It operates at the same LuaJIT runtime layer, can directly replace the standard LuaJIT runtime in an OpenResty deployment, and is part of the OpenResty private libraries family. It is not another Lua syntax mode in luajit2, nor is it an alias for GC64; it is an enhanced runtime for production environments. Check the private-library documentation for exact versions, distribution methods, and compatibility.
LuaJIT’s GC reclaims Lua objects, but that does not guarantee that the allocator immediately returns the underlying physical pages to the operating system. As a result, a long-running service that creates and releases many short-lived objects can show healthy Lua GC metrics while its RSS keeps increasing. LuaJIT-plus addresses this allocator-level problem by adding active management of idle, fragmented memory, bringing RSS closer to the actual working set and reducing the risk of OOM kills caused by inflated RSS. See Fixing LuaJIT RSS Memory Leaks in OpenResty for diagnostic methods, allocator behavior, and production data.
The boundary between the two can be understood as follows: ordinary OpenResty applications can use the luajit2 bundled with the release; services with steadily increasing RSS, difficult memory limits, or a need for a more predictable long-running memory model can evaluate LuaJIT-plus. Because it replaces the runtime, it must be adopted through the corresponding build or deployment process; its active memory-return mechanism, however, works while the service is running and is not the same as releasing memory through scheduled restarts. LuaJIT-plus also includes the high-performance JSON library jit.cjson as a companion capability.
FFI and OpenResty APIs
LuaJIT includes FFI, which lets Lua code call C functions and work with C data structures. OpenResty’s distribution includes lua-resty-core, which reimplements parts of the ngx.* and ndk.* APIs using FFI; these implementations can work with LuaJIT’s JIT compiler. The library is included with OpenResty by default, and the version bundled with the corresponding release is recommended for compatibility.
LuaJIT vs luajit2: Frequently Asked Questions
What is the difference between LuaJIT and luajit2?
luajit2 is OpenResty’s maintained branch of LuaJIT 2.1, not a separate language: it runs the same Lua 5.1 code and regularly synchronizes upstream changes. On top of upstream, it adds table APIs such as table.isempty and table.nkeys, larger JIT defaults for large applications, and C APIs used by the OpenResty core. The exact differences depend on the branch revision bundled with a release.
Should I install upstream LuaJIT or luajit2 for OpenResty?
For a standard OpenResty deployment, use the LuaJIT version bundled with that OpenResty release. Its LuaJIT, lua-nginx-module, and related components are packaged together. lua-nginx-module also supports official LuaJIT 2.x releases, but its documentation notes that performance will be significantly lower, and Nginx logs an alert when it detects a LuaJIT build that is not OpenResty’s.
What is the difference between luajit2 and LuaJIT-plus?
luajit2 is the LuaJIT branch bundled with open-source OpenResty releases. LuaJIT-plus is a separately distributed enhanced runtime for production memory management; it addresses allocator behavior that can keep RSS high after Lua objects have been collected. It is not another name for luajit2 or for GC64.
Continue Reading by Problem
- If loading Lua modules at startup is slow, see Precompiling Lua Modules to LuaJIT Bytecode.
- If LuaJIT’s GC-managed memory limit is the issue, see LuaJIT GC64 Mode.
- If RSS keeps increasing while Lua GC metrics look normal, see LuaJIT RSS Memory Problems and LuaJIT-plus.
- If you want to locate Lua CPU hot spots in production, see Lua CPU Flame Graphs in Practice.
- If you want to inspect JIT compilation logs or measure code execution time, see the
restyCommand-Line Tool and Measuring Lua Code Execution Time.
About the Author
Yichun Zhang is the founder of the open-source OpenResty project and the founder and CEO of OpenResty Inc..












