lualatex is not LuaTeX. Follow the symlink on TeX Live 2024 and it points not at luatex but at luahbtex — a different binary, with HarfBuzz built in. fmtutil.cnf says the same thing: lualatex luahbtex ... lualatex.ini, so even the format is dumped by LuaHBTeX. Run it and the banner announces itself as This is LuaHBTeX, Version 1.18.0 (TeX Live 2024), which is easy to read past. Starting from what the LaTeX engine actually is, this page goes through where Lua reaches into the typesetting (\directlua and the callbacks), what LuaJITTeX has become, and how much of the reputation that LuaLaTeX is slow survives an actual measurement.
lualatex is LuaHBTeX: what HarfBuzz adds
It carries the whole of HarfBuzz inside the binary. LuaHBTeX entered TeX Live in 2020, and that is when the thing behind lualatex was swapped out; the release notes were explicit that LuaTeX itself stayed ordinary LuaTeX and only the LaTeX format changed. One line settles it: compile \directlua{tex.print(status.luatex_engine)} with lualatex and the page reads luahbtex; compile it with luatex and it reads luatex. Try require("luaharfbuzz") and it succeeds under luahbtex, reporting HarfBuzz 8.3.0, while plain luatex fails with module 'luaharfbuzz' not found. HarfBuzz is not a file sitting somewhere on disk; it is linked statically into the executable, which is why no package can add it after the fact — and why it ships as a separate binary at all.
$ readlink $(which lualatex)
luahbtex
$ grep -E '^lualatex ' $(kpsewhich fmtutil.cnf)
lualatex luahbtex language.dat,language.dat.lua lualatex.ini
$ lualatex --version | head -1
This is LuaHBTeX, Version 1.18.0 (TeX Live 2024)
# Ask the engine what it is, from inside a document:
% \directlua{tex.print(status.luatex_engine)} -> luahbtex
% \directlua{tex.print(_VERSION)} -> Lua 5.3
% \directlua{local hb = require("luaharfbuzz")
% tex.print(hb.version())} -> 8.3.0The practical consequence: there are two shaping paths, and by default luaotfload uses its own shaper written in Lua. Ask for Renderer=Harfbuzz in fontspec and it switches to HarfBuzz — the log then shows entries such as luaotfload.harf.finalize_hlist being inserted into hpack_filter and post_linebreak_filter. For scripts whose shaping is intricate, Tibetan or Bengali among them, HarfBuzz is the more accurate of the two, and that was the reason LuaHBTeX was made in the first place. For a document set entirely in Latin script you will not see a difference.
Who built LuaTeX, and what for
Type luatex --credits and the development team appears as Hans Hagen, Hartmut Henkel, Taco Hoekwater and Luigi Scarso, followed by a list of the projects LuaTeX absorbed: TeX (Knuth), e-TeX (Peter Breitenlohner and friends), Omega (John Plaice, Yannis Haralambous), Aleph (Giuseppe Bilotta), pdfTeX (Hàn Thế Thành), MetaPost, Lua, and — annotated “used in LuajitTeX” — LuaJIT (Mike Pall). The strand of Omega and Aleph, the multilingual engines, is the part people rarely know about. Development began in the ConTeXt world and in the Oriental TeX project, an effort aimed at scholarly Arabic typesetting and critical editions in particular, in which a grant proposed by Idris Samawi Hamid of the Department of Philosophy at Colorado State University funded the core reprogramming; the Dutch NTG, TUG and Germany's DANTE contributed as well. The idea of making TeX programmable came out of a concrete wish to set Arabic beautifully.
Because pdfTeX is its base, LuaTeX writes PDF directly and inherits the e-TeX extensions along with most of pdfTeX's typesetting machinery. The \pdf... namespace was tidied up, though: much of it now goes through three gateways — \pdfextension, \pdfvariable, \pdffeedback — which take a keyword and arguments, while \pdfoutput became \outputmode and \pdfximage became \saveimageresource. You will rarely write any of these by hand; LaTeX packages absorb the difference. Input is UTF-8 from the start, and fonts are named from the operating system with fontspec — \setmainfont, \setsansfont, \setmonofont — exactly as in XeTeX, but the loading is done by LuaTeX's own Lua-based loader, luaotfload, an adaptation of ConTeXt's font loader for Plain TeX and LaTeX, now maintained by the LaTeX team's latex3 group.
\directlua: running Lua in the middle of a document
\directlua{...} runs the Lua inside it immediately, and whatever tex.print(...) sends back re-enters TeX's input stream and is typeset as ordinary text. There is one thing here that catches everybody the first time they try it: \directlua{tex.print(2^10)} prints not 1024 but 1024.0. In Lua 5.3, which is what LuaTeX uses, ^ always returns a float. Write math.tointeger(2^10) when you want the integer. The same property bites when a Lua result is assigned to a dimension or a counter, and the LuaTeX manual warns explicitly that tostring and string.format may return scientific notation and confuse the TeX end of things.
\documentclass{article}
\begin{document}
% careful: 2^10 is a float in Lua 5.3, so this prints 1024.0
Two to the tenth is \directlua{tex.print(2^10)}.
% and this prints 1024
Two to the tenth is \directlua{tex.print(math.tointeger(2^10))}.
\end{document}Its sibling \latelua{...} runs instead when the page it sits on is shipped out. Work that cannot happen until page numbers and final coordinates are fixed — drawing onto the PDF, attaching an annotation — belongs there. Compile a document containing both and the log shows the difference plainly: the output of \directlua appears before [1, and the output of \latelua after it. One practical note: raw \directlua is fussy about braces and special characters, so for anything longer than a line use the luacode environment from the luacode package, where Lua can be pasted in verbatim and safely.
Callbacks: inserting your own function into the typesetting
Callbacks are where LuaTeX's real power lives. TeX assembles text as a linked list of nodes — characters, boxes and glue, the stretchable space — and by registering a callback you have your own Lua function invoked at each such juncture, free to inspect and rewrite that node list. The LuaTeX in TeX Live 2024 exposes 74 callbacks, which the manual sorts into six groups: file discovery, data processing, node-list processing, information reporting, PDF-related and font-related. The example below actually runs: it counts how many lines a paragraph was broken into and reports the total at the end of the run.
\documentclass{article}
\usepackage{luacode}
\begin{luacode*}
local count = 0
luatexbase.add_to_callback("post_linebreak_filter",
function(head)
for line in node.traverse_id(node.id("hlist"), head) do
count = count + 1
end
return head
end, "count lines")
luatexbase.add_to_callback("stop_run",
function() texio.write_nl("LINES TYPESET: " .. count) end, "report")
\end{luacode*}
\begin{document}
This paragraph is broken into lines by TeX, and the Lua function
registered on post\_linebreak\_filter counts them as they go past.
\end{document}The example registers through luatexbase.add_to_callback rather than the raw callback.register, because the raw form allows only one function per callback. In the LaTeX world several packages want to intervene at the same point, so luatexbase gathers them into an ordered list. The tools for working with nodes are a set of tables visible from Lua: tex is the window onto TeX's internal state (registers and dimensions), node creates, walks and frees nodes, token deals with tokens (TeX's smallest units of meaning), font with font data, and status with runtime information. Even advanced font machinery such as luaotfload is written in Lua on top of these.
| Callback | When it fires | Typical use |
|---|---|---|
process_input_buffer | as each input line is read | preprocessing input (takes a string, returns a string) |
pre_linebreak_filter | just before a paragraph is broken into lines | rewriting the node list before line breaking |
post_linebreak_filter | immediately after line breaking | acting on the finished lines |
hpack_filter | each time a horizontal box is assembled | adjusting the contents of a box after the fact |
ligaturing / kerning | the ligature- and kerning-building stages | replacing the font's own fitting |
stop_run | at the very end of the run | reporting totals, cleaning up |
What has become of LuaJITTeX
It has not disappeared. TeX Live 2024 ships two executables, luajittex and luajithbtex, both at version 1.18.0, and fmtutil.cnf defines formats for them. What is inside is not reference Lua but LuaJIT, a just-in-time implementation that compiles to machine code at run time. Ask it directly and jit.version answers LuaJIT 2.1.0-beta3 while _VERSION answers Lua 5.1. LuaJITTeX is therefore pinned to the 5.1 language level and runs on a different language specification from mainline LuaTeX, which is at 5.3. The LuaTeX manual says as much: LuaJIT is not in sync with regular Lua development, so LuaJITTeX lags behind.
This difference is not academic. Compile the identical line \directlua{tex.print(2^10)} with all four binaries and luatex and luahbtex print 1024.0 while luajittex and luajithbtex print 1024 — because Lua 5.3 distinguishes integers from floats and 5.1 does not. Changing the engine changes what the document says, which is one reason not to recommend LuaJITTeX as a drop-in replacement for LuaLaTeX. There is a second and more decisive fact: fmtutil.cnf defines only Plain-style formats for the LuaJIT engines. Nothing corresponding to luajitlatex exists. Running LaTeX on top of LuaJIT means building your own format, so the accurate statement is that it is not, in practice, an option on a LaTeX user's menu.
A second line of succession is LuaMetaTeX, a leaner rewrite of LuaTeX that underpins modern ConTeXt (LMTX / MkXL). It brings reworked mathematical typesetting, a large number of new primitives, and the Lua 5.4 line. TeX Live 2024 ships luametatex 2.11.02, and the context command is itself a symbolic link to it. The division of labour is clean: LuaTeX for LaTeX work, LuaMetaTeX for current ConTeXt.
Is LuaLaTeX really slow? A measurement
It is genuinely slower, but people misplace where the cost lives. Start-up is nearly identical: on a near-empty one-page document, pdfLaTeX took 0.20 s, XeLaTeX 0.30 s and LuaLaTeX 0.32 s (same machine, fastest of several runs). The gap opens in the part that scales with the amount of typesetting. On the same 417-page mathematical document, pdfLaTeX took 0.40 s, XeLaTeX 0.53 s and LuaLaTeX 1.41 s. Subtract the start-up and look at the typesetting alone: XeLaTeX costs about 1.2 times pdfLaTeX, LuaLaTeX more than five times. Turn that around and it means the difference is imperceptible on a ten-page paper.
| Measurement | pdfLaTeX | XeLaTeX | LuaLaTeX |
|---|---|---|---|
1-page document | 0.20 s | 0.30 s | 0.32 s |
417-page document | 0.40 s | 0.53 s | 1.41 s |
typesetting only | baseline | about 1.2x | about 5.6x |
Three practical responses. First, while drafting, cut the number of round trips rather than chasing raw speed — an automated build such as latexmk that recompiles only what changed does more for you than the choice of engine. Second, do not confuse this with the one-off font-cache cost. When luaotfload meets a font it has not seen, it scans and indexes, and that single run is dramatically slower. Most reports of “LuaLaTeX taking tens of seconds” are this; from the second run on you are back to the numbers above. Third, in CI, count round trips and reruns rather than the cost of one compile. If your build runs two or three passes for cross-references and a table of contents, whatever gap exists per pass is multiplied by three.
MetaPost built in, and Japanese with LuaTeX-ja
LuaTeX has the MetaPost drawing engine built in as the library MPlib, so figures can be generated inside the same process without calling an external program. From LaTeX you reach it through the luamplib package and write MetaPost code directly inside an mplibcode environment. Compile one and inspect the log: there is no trace of an external mpost being launched anywhere. That quietly matters — it means figures still build in environments with shell escape switched off, and in CI.
Japanese typesetting is handled by LuaTeX-ja (package luatexja). It reimplements, on the Lua side and through callbacks, the Japanese-typesetting knowledge pTeX had — vertical writing, the inter-character spacing and punctuation handling driven by JFM (Japanese Font Metrics), and the spacing between Japanese and Western text — which makes it the largest working example of the callback machinery described above. With luatexja-fontspec you can select Japanese fonts in fontspec's style too. One caution: LuaHBTeX's HarfBuzz shaping and LuaTeX-ja's vertical-writing and CID machinery should be combined carefully. The LuaTeX-ja manual warns that Japanese fonts defined through HarfBuzz can produce unwanted results. For a real manuscript, first get a PDF out using TeX Live's bundled Harano Aji fonts and the standard settings, and introduce OpenType features or HarfBuzz options only after testing them in something small.
\documentclass{ltjsarticle}
\usepackage{luatexja-fontspec}
% Harano Aji ships with TeX Live, so this builds anywhere
\setmainjfont{HaranoAjiMincho-Regular}
\setsansjfont{HaranoAjiGothic-Medium}
\begin{document}
\section{日本語}
Text and mathematics $E=mc^2$ go through the same engine.
\end{document}Starting a new document in LuaLaTeX
- Start from Unicode source. No
inputenc, nofontenc. Migrating with the pdfLaTeX-era declarations still in place buys you warnings and a duplicated font setup. - Make
fontspecthe single source of font selection. For a shared manuscript, first get a build that works with TeX Live's bundled fonts, then substitute. - Use
luatexjafor Japanese. It covers vertical writing, JFM and the spacing between Japanese and Western text. - Put longer Lua in a
luacodeenvironment. Raw\directluais fussy about braces and special characters. - Register callbacks through
luatexbase.add_to_callback. The rawcallback.registerallows only one function per hook. - Build a small sample first. One page that exercises the body font, the Japanese font, mathematics and the bibliography — then use it as your template.
The one-line rule of thumb: LuaLaTeX if you want to reach into the typesetting itself, XeLaTeX if you only want operating-system fonts by name, pdfLaTeX if the work is mostly English and speed and compatibility matter. A page comparing the three head to head exists separately. Since the LaTeX team targets LuaTeX for new development, LuaLaTeX is edging toward being the default for new Unicode/OpenType workflows — particularly the ones where you want to program something.