All Your GUCs in a Row: jit and jit_provider

We arrive at the jit_* family, and here the alphabet stops being a useful editor. Strict ordering would put the cost thresholds before the concepts they gate and leave jit_provider stranded at the far end, so for this family we’re going in logical clusters instead: the switch and the engine today, then the pair that decides what gets compiled, then the three cost thresholds, and the debugging toggles last.

jit is the switch. Boolean, context user, default on. It arrived in PostgreSQL 11 turned off, and PostgreSQL 12 turned it on, where it has stayed. When it’s on and the planner estimates a query to be expensive enough (the thresholds are the next post’s business), the executor compiles the query’s expression evaluation and tuple deforming into native code via LLVM before running it. Compilation costs real time up front in exchange for faster execution per row, an exchange that pays on long, CPU-bound analytical queries and pays nothing on short ones. Whether it fired, and what it cost, appears at the bottom of EXPLAIN ANALYZE:

1 JIT:
2 Functions: 11
3 Options: Inlining false, Optimization false, Expressions true, Deforming true
4 Timing: Generation 0.538 ms (Deform 0.111 ms), Inlining 0.000 ms,
5 Optimization 0.557 ms, Emission 14.932 ms, Total 16.027 ms

One observability trap before moving on: EXPLAIN (ANALYZE, COSTS OFF) suppresses the entire JIT section, not just the cost estimates. Since COSTS OFF is the standard courtesy when sharing plans, a plan with no JIT block proves nothing about whether JIT ran. (TIMING OFF keeps the block and drops only the timing line.)

jit_provider names the shared library that does the compiling. Context postmaster, default llvmjit, added in 11 alongside everything else. The interface is formally pluggable, and people have built toy providers to prove it, but llvmjit is the only one that exists in practice, and inlining only works with it anyway, because inlining depends on the LLVM bitcode the build process emits. You will never set this parameter. The reason it earns space in this post is what it points at: a library that is not necessarily installed.

On the PGDG Debian and Ubuntu packages, llvmjit.so lives in a separate package, postgresql-18-jit, and the server package only recommends it, so any installation with --no-install-recommends, which describes most Docker images, quietly lacks it. On the RPM side the split is the same and more severe: postgresql18-llvmjit is deliberately not pulled in at all. And here is how PostgreSQL behaves when the library named by jit_provider doesn’t exist:

1postgres=# SHOW jit;
2 jit
3-----
4 on
5
6postgres=# SELECT pg_jit_available();
7 pg_jit_available
8------------------
9 f

jit says on. Queries plan and run normally, with no JIT section in their plans. Nothing appears in the server log; nothing errors; nothing warns. The documentation is explicit that this is intended: if the library is absent, “no error will be raised.” The configuration says one thing, the server does another, and the only witness is pg_jit_available(), which returns true only when jit is on and the provider actually loads. (It returns false when jit is off, too, so test with the switch on.)

The recommendation for this pair is short, because the interesting decision, whether JIT should be enabled for your workload at all, belongs with the cost thresholds. Leave jit_provider at its default until the day you write a JIT provider. And run SELECT pg_jit_available() on every server you operate, because jit = on in a config file is an intention, not a fact, and PostgreSQL will never tell you the difference on its own. More than one fleet has “enabled” JIT for years without ever compiling an instruction.