skip to content

In CRuby, which kinds of workload does YJIT speed up, and when does enabling it barely help or even cost you?

level: middleimportance: should knowfreq 45%

answer

  1. only Ruby-level CPU time shrinks
  2. I/O waits stay the same
  3. C code is already native
  4. short scripts never warm up
  5. TracePoint and binding deoptimise

basics

~20 s

YJIT speeds up long-running processes that spend their time executing Ruby code: method calls, loops, object building. It barely helps time spent waiting on I/O, inside C extensions or in GC, and can cost more than it saves in short-lived processes.

solid answer

~40 s

A JIT can only shorten time spent **executing Ruby bytecode**. So YJIT helps long-lived processes whose profile is dominated by Ruby-level work: many small method calls, loops, hash and object manipulation, template rendering. It does little for time spent **waiting** on a database or network, for work inside **C functions** (a C extension's parser is already machine code), or for garbage collection. It can even lose on **short-lived processes**: every method first runs interpreted until the call threshold, and compile work is wasted if a worker is restarted often. Code that uses `TracePoint` or `binding`, redefines core operators, or passes changing types through the same variables also gets less from it. And it always costs memory, which matters in tight containers.

go deeper

for a junior

Recall that a JIT only speeds up running Ruby code, not waiting on I/O, and that it pays off in long-running processes.

for a middle

Explain the time buckets - Ruby bytecode, I/O waits, C functions, GC, boot - and which one YJIT touches.

for a senior

Show you decide from a profile, name the things that defeat YJIT, and weigh worker restarts and memory limits before enabling it.

for a principal

Frame JIT adoption as a per-service decision driven by the share of Ruby CPU time and the memory headroom of each deployment.

## What a JIT can and cannot make faster YJIT compiles **YARV bytecode** - the instructions CRuby produces from Ruby source - into machine code. That makes one thing faster: the CPU time the interpreter would spend dispatching and type-checking those instructions. Everything else a request or job does is outside its reach. A useful way to reason about it is to split wall-clock time into buckets: | Where time goes | Does YJIT shorten it? | Why | |---|---|---| | Executing Ruby methods, blocks, loops | Yes, often substantially | this is the bytecode YJIT compiles | | Waiting on a database, HTTP call, disk | No | the process is idle; nothing is executing | | Inside C functions and C extensions | Little | that code is already machine code; YJIT can only trim the call overhead around it | | Garbage collection | No | the collector is C code inside the VM | | Boot and one-off setup | Often negative | code runs too few times to be compiled, or is compiled and never reused | The overall speed-up is therefore bounded by the share of time in the first row. A request that spends 80% of its time waiting on queries can gain at most a slice of the other 20%. ## Workloads that tend to benefit - **Long-running server processes** that execute the same Ruby code millions of times: routing, serialisation code that builds hashes and arrays, view rendering, business logic. - **Background workers** that loop over many jobs of the same kind. - **CPU-bound Ruby code** such as parsers, template engines or data transformations written in Ruby rather than in C. - Code whose variables and arguments **keep the same types**: YJIT's documentation recommends exactly this, because its compiled blocks are specialised by type. ## Workloads that gain little or lose 1. **Short-lived processes.** A CLI command or a test run that lasts a couple of seconds calls most methods fewer than 30 times (the default `--yjit-call-threshold`), so they are never compiled, yet YJIT still reserves memory. 2. **Frequently restarted workers.** YJIT's own production tips say that if processes are killed too often, compile time can outweigh the speed-up, and advise reducing the killing frequency. 3. **I/O-bound services.** Time spent waiting is untouched. 4. **C-heavy hot paths.** When a profile shows most time inside a C extension, YJIT has little to compile. 5. **Code that defeats its assumptions.** The documentation lists `TracePoint` and `binding` as causes of deoptimisation, and warns against redefining basic integer operators or the meaning of `nil` and equality. Heavy use of `OpenStruct` and deep layers of trivial wrapper methods are also on its list of things to avoid in hot code. 6. **Memory-bound deployments.** YJIT adds up to `--yjit-mem-size` (128 MiB by default in Ruby 4.0) per process. When a container is already near its limit, the extra memory can cost more than the CPU saved. ## How to decide - Profile first and look at where time goes; a JIT decision without a profile is a guess. - Look for a large share of time in Ruby frames, not in waits or C functions. - Try it on real traffic and compare latency, CPU per request and memory per process. - Check that `ratio_in_yjit` is high when you can measure it; YJIT's documentation says it should ideally approach 99%. ## Summary YJIT is a **CPU-for-memory trade on Ruby-level execution time**. It shines on long-lived processes running hot Ruby code, and it is neutral to negative on short runs, wait-dominated work, C-dominated work, and memory-starved hosts.

  • A hot endpoint spends most of its CPU inside a C extension that parses JSON; what will YJIT change?
    Very little on that path. The parser's C code is already machine code, so YJIT can only speed the Ruby code around it, such as building the objects to encode or walking the result. If most time is inside the C function, look for a faster library or less data, not a JIT.
  • Why can YJIT help a server process but slow down a test suite?
    A server runs the same methods millions of times, so compile work is repaid many times over. A test run touches thousands of methods only a few times each: most never reach the 30-call threshold, and the ones that do are compiled and then barely reused, while the process still pays YJIT's memory and compile costs.

A JIT is like a delivery driver who memorises the routes they drive every day: the regular runs get faster, a one-off delivery gains nothing, and no amount of route knowledge shortens the wait at a customer's door.

saying these in an interview costs you the question

  • YJIT speeds up database-bound requests because queries run faster
  • A JIT makes C extensions faster by recompiling them
  • YJIT always helps, so every Ruby process should run it
  • YJIT lets CRuby threads run Ruby code in parallel
  • Short scripts benefit most because YJIT compiles at startup