A CPU-heavy Ruby worker that renders PDFs keeps one core busy on CRuby with eight threads; when would you move it to JRuby or TruffleRuby, and what breaks?
answer
- threads vs cores on CRuby
- JVM threads run Ruby in parallel
- no C extensions on JRuby
- warm-up and start-up cost
- benchmark after warm-up, not cold
basics
~20 sMove a long-running CPU-bound worker to JRuby or TruffleRuby when one process must use many cores, since their threads run Ruby in parallel; expect broken native gems (JRuby), no fork, slower start-up, warm-up and higher memory.
solid answer
~40 sOn CRuby, eight threads doing pure-Ruby layout work still execute Ruby code one at a time, so the usual CRuby answer is more **processes**, not threads. **JRuby** maps Ruby threads to JVM threads that run in parallel, and TruffleRuby also runs Ruby threads in parallel, so one process can use every core. I would consider moving only for a **long-lived** worker whose hot path is pure Ruby, and only after benchmarking warmed-up throughput per core and per gigabyte. What breaks: gems with **C extensions** do not load on JRuby (they need `java`-platform builds); `fork` and MRI-only APIs such as `RubyVM` are unavailable; start-up and **JIT warm-up** hurt short jobs; memory per process is higher; and code that was never thread-safe starts failing under real parallelism.
code
ruby · 18 linesworkers = Integer(ENV.fetch("RENDER_THREADS", "8"))
if RUBY_ENGINE == "ruby"
warn "CRuby: threads share one core for Ruby code; run #{workers} processes instead"
end
queue = Thread::Queue.new
jobs.each { |job| queue << job }
queue.close
threads = Array.new(workers) do
Thread.new do
while (job = queue.pop)
render_pdf(job) # pure-Ruby layout: parallel on JRuby/TruffleRuby
end
end
end
threads.each(&:join)go deeper
Recall that CRuby threads do not run Ruby code on several cores at once, and that JRuby and TruffleRuby threads do.
Explain the CRuby alternatives, more processes or experimental Ractors, and why JRuby cannot load gems with C extensions.
Show the decision process: profile the hot path, audit native gems and fork, benchmark warmed-up throughput per core and per gigabyte, and expect latent races to surface under real parallelism.
Weigh the organisational cost of a second runtime, a JDK, new CI jobs and new debugging skills, against the capacity it saves, and prefer scaling CRuby processes unless the gain is large and durable.
## The situation A background worker renders invoices or reports into PDFs. Profiling shows the time goes into pure-Ruby layout and drawing code, not waiting on the network. The team gave it eight threads, but on CRuby only one core is busy: CRuby lets one thread execute Ruby code at a time, so threads help mainly while they wait on I/O. The question is whether a different **engine** is the fix. ## Options on CRuby first Before changing engines, list what CRuby offers, because staying on the reference engine is the cheapest path: 1. **More processes.** Run several worker processes (or a pre-forking supervisor), each using one core. This is the standard CRuby answer; the cost is memory per process. 2. **Ractors.** CRuby's actor model can run Ruby in parallel inside one process, but in 4.0 it is still **experimental** and demands shareable objects, which most rendering libraries are not written for. 3. **Make the hot path cheaper.** Enabling YJIT, allocating less, or moving the heaviest loop into a native library can change the picture without an engine switch. ## What JRuby and TruffleRuby change - **Parallel threads.** JRuby runs Ruby threads on JVM threads that execute truly in parallel, and TruffleRuby also runs Ruby threads in parallel. Eight threads can keep eight cores busy in **one** process, sharing one heap. - **Peak speed after warm-up.** Both engines compile hot code with JIT compilers (the JVM's, or Graal on TruffleRuby). A worker that runs for hours amortises warm-up; a job that starts, renders one file and exits does not. - **Different memory profile.** JVM-based engines start with a larger baseline heap, but one parallel process can replace several CRuby processes, so compare **total** memory for the same throughput. ## JRuby or TruffleRuby? If the answer is "switch", the two engines are not interchangeable. JRuby is the more common choice when the team already runs JVM services, wants Java libraries for PDF or image work, or needs mature JVM monitoring. TruffleRuby is attractive when the dependency tree has C-extension gems that have no `java` build, because it compiles them itself, and when very long-running processes can absorb its longer warm-up in exchange for peak speed. Either way, the engine's `RUBY_VERSION` decides which Ruby language features the code may use, so a codebase that already relies on the newest CRuby 4.0 syntax may need to wait. ## What breaks when you switch | Area | JRuby | TruffleRuby | |---|---|---| | Gems with C extensions | do not load; need a `java`-platform gem or a pure-Ruby alternative | built from the `ruby`-platform gem; usually work, sometimes slower | | `fork` and pre-fork servers | not supported | not supported | | MRI-only APIs (`RubyVM`, YJIT switches) | absent | absent | | Start-up time | slow (JVM boot) | slow, plus longer warm-up | | Language version | its own `RUBY_VERSION`, may lag CRuby 4.0 | its own `RUBY_VERSION`, may lag CRuby 4.0 | Two more failures are behavioural rather than missing features: - **Latent thread-safety bugs.** Code that shared a memoised Hash or a lazily built object between threads may never have raced under CRuby's one-at-a-time execution. Under true parallelism those races become corrupted state or intermittent exceptions. - **The lockfile and CI.** A `java`-platform lockfile entry, a JDK in the container image and a CI job on the new engine are all new infrastructure the team now owns. ## How to decide 1. **Confirm the bottleneck** with a profiler: the time must be in Ruby code, not in a native library or I/O. 2. **Audit the dependency tree** for native extensions and `fork`, and find replacements before any benchmark. 3. **Benchmark warmed-up throughput** with production-like documents: pages per second per core and per gigabyte, after the JIT has settled, against the multi-process CRuby setup. 4. **Weigh the operating cost**: a second runtime in production, a JDK to patch, and engineers who must debug JVM heap and GC settings. Measure the CRuby baseline honestly as well: several processes with YJIT enabled are the real competitor, not a single process with eight idle threads. If the rendering library spends most of its time inside a native extension, the engine barely matters, and on JRuby that same extension may not load at all. A good interview answer ends with a condition, not a slogan: switch when a long-lived, pure-Ruby, CPU-bound worker shows a measured win that outweighs running a second engine; otherwise scale CRuby processes.
- What stops a working CRuby app from running on JRuby most often?Native extensions. JRuby does not load CRuby C extensions, so every gem that compiles C code needs a `java`-platform release or a pure-Ruby replacement. The second most common blocker is code that calls `fork` or MRI-only APIs such as `RubyVM`, which JRuby does not provide.
- Why can moving to parallel threads expose bugs that never appeared on CRuby?On CRuby only one thread executes Ruby code at a time, so many unsynchronised read-modify-write sequences rarely interleave in practice. On JRuby or TruffleRuby threads really run simultaneously, so shared memoised hashes, counters and lazily initialised objects race, producing lost updates or intermittent exceptions. The code was always unsafe; the engine just stops hiding it.
- When is JRuby a poor fit even for CPU-heavy work?When jobs are short-lived. JVM start-up and JIT warm-up dominate a process that renders one file and exits, so it never reaches the parallel peak speed. It also fits poorly when key dependencies are C-extension gems without `java` builds.
saying these in an interview costs you the question
- Adding threads on CRuby will spread pure-Ruby CPU work across cores
- JRuby runs every CRuby gem, including ones with C extensions
- Switching engines is safe because the Ruby syntax is identical
- JRuby is faster for every workload, including one-shot scripts
- Code that never raced on CRuby is proven thread-safe