The third cluster of the jit_* family is the one that decides when JIT happens, and it is where the trouble lives. All three arrived with the subsystem in PostgreSQL 11, all three have context user, and all three are denominated in planner cost units, that dimensionless currency anchored to seq_page_cost = 1.0. When the planner finishes a plan, it compares the plan’s total estimated cost against these thresholds and stamps the result into the plan itself. Cost above jit_above_cost (default 100000): compile the expressions and tuple deforming. Above jit_inline_above_cost (default 500000): also inline the bodies of built-in functions and operators, pulled from the LLVM bitcode installed alongside the server. Above jit_optimize_above_cost (also 500000): also run LLVM’s expensive optimization passes over the result. Setting any of them to -1 disables that tier; jit_above_cost = -1 disables the whole thing, since the other two tiers build on the first.

Three consequences follow from that design, and they compound. The decision is made from the estimated cost, so a misestimate can buy compilation for a query that runs in two milliseconds. The compiled code is not cached: compilation happens at executor startup, on every execution, in every process, and parallel workers each compile their own copy. And the currency is wrong: planner cost measures pages fetched and rows processed, while JIT’s benefit depends on how many times how-complex expressions execute. Those quantities are correlated for the workload JIT was built for, big scans with heavy predicates, and uncorrelated almost everywhere else.

Here is what uncorrelated looks like, at stock settings, with nothing tuned. A 550MB table in 64 hash partitions, and the simplest query it can be asked:

1EXPLAIN ANALYZE SELECT sum(id) FROM bigp;
2
3 Aggregate (cost=112620.00..112620.01 rows=1 width=8) ...
4 -> Append (cost=0.00..106620.00 rows=2400000 width=4) ...
5 [64 partition scans]
6 JIT:
7 Functions: 130
8 Options: Inlining false, Optimization false, Expressions true, Deforming true
9 Timing: Generation 2.820 ms (Deform 1.333 ms), Inlining 0.000 ms,
10 Optimization 1.665 ms, Emission 44.539 ms, Total 49.023 ms
11 Execution Time: 938.443 ms

The estimate of 112620 clears 100000, so JIT fires. The plan has sixty-four scan nodes, so instead of the five functions the unpartitioned version of this query compiles, it compiles 130, and pays for them on every execution. The same query on the same data with jit = off ran in 844 to 878 ms warm; with JIT it ran in 871 to 938 ms plus the 36 to 49 ms of compilation shown above. JIT charged admission and improved nothing, because a sum(id) has almost no expression work to accelerate, no matter what the cost estimate says. Partitioning is the classic amplifier here, inflating the estimate and the function count simultaneously, but any query whose cost comes from data volume rather than expression complexity gets the same deal. And this demo shows the mild tiers. When an estimate clears 500000 and inlining and optimization join in, compilation stops being milliseconds: on my test instance, a forced run with all thresholds at zero spent 2.8 seconds inlining and 0.25 seconds optimizing (summed across a parallel leader and two workers) to accelerate a query that executes in 1.3.

Whether any of this is hurting you is a query away, because pg_stat_statements has carried per-statement JIT counters since PostgreSQL 15:

1SELECT sum(total_exec_time) AS exec_ms,
2 sum(jit_generation_time + jit_inlining_time +
3 jit_optimization_time + jit_emission_time) AS jit_ms
4FROM pg_stat_statements;

If jit_ms is a visible fraction of exec_ms on a transactional workload, you are paying the compiler to attend queries that don’t need it.

The recommendation practically writes itself, and it is not “tune the thresholds.” The thresholds are measuring the wrong thing, and moving them adjusts where the wrong measurement bites, so make the decision at the workload level instead: ALTER DATABASE oltp_db SET jit = off for transactional databases, jit = on for the warehouse, and both sides get the right answer without a restart. If a single database hosts both personalities, raising jit_above_cost by an order of magnitude is a defensible bluntness, and splitting the two 500000 thresholds from each other almost never earns its complexity. The one move with no defense is lowering any of them: the defaults already fire on a partitioned sum(id), and they do not need the encouragement.