January 13, 2026·4 min read·engineeringrubyconcurrencyruntime

Ruby 4.0.1 repairs sleep interrupted by an unrelated subprocess

Ruby 4.0.1 fixes a regression where an unrelated child process could end an indefinite sleep. The failure came from thread-state bookkeeping around signal handling.

An indefinite sleep in Ruby 4.0.0 could return when a subprocess exited in another thread. The application did not explicitly wake the sleeping thread, yet its execution resumed. Ruby 4.0.1, released on January 13, 2026, corrects that regression. The release announcement gives this small-looking behavior priority over the major version's more conspicuous additions.

The original bug report distinguishes indefinite sleep from timed sleep. Calling sleep without an argument failed in the reported Ruby 4.0.0 environment, while sleep(N) was unaffected. The reporter compared it with Ruby 3.4.8, where the indefinite sleep remained blocked. That narrow comparison gave maintainers a concrete regression to investigate.

I came to Ruby through Perl and still appreciate its more consistent object model and readable collection operations. My affection is qualified, especially when deciding why a new system should use it. This patch is a useful reminder that the most consequential language work can concern a long-established operation. Code written in plain Ruby still depends on the runtime maintaining its scheduling promises.

A signal woke the thread without ending the wait

The report's reproducer starts a thread that briefly sleeps and then executes a subprocess through backticks. Meanwhile, the main thread calls sleep without an argument. On the affected runtime, the subprocess's exit causes the main sleep to return zero. The program then continues into code that should not have run merely because another thread finished a child process.

The failure involves SIGCHLD, the child-process status signal. According to the maintainer's explanation, Ruby's timer thread wakes the main thread when checking for signals. The main thread runs a registered Ruby signal handler if one exists and should then return to sleeping. An internal wakeup for signal processing is therefore different from a request to complete the application's indefinite sleep.

The regression changed thread-status writes around a scheduler wait. Those writes were generally appropriate, but indefinite sleep already managed its status through sleep_forever. That routine loops while checking the thread's status. After the signal-related wakeup, the changed status made the loop continue application execution instead of putting the main thread back to sleep.

This is a concrete example of an internal notification acquiring the wrong application meaning. The signal-processing path needed the thread to run temporarily. The sleeping operation still needed to preserve its waiting condition afterwards. A status field shared by those paths connected the two behaviors in a way that the original change did not handle.

The reproducer separates two kinds of sleep

A shortened illustration of the reported structure makes the distinction visible. It is a diagnostic example for the affected runtime, not an application pattern for coordinating threads.

Thread.new do
  sleep 0.3
  `echo ready`
end

sleep
puts "The indefinite sleep returned"

The timed sleep belongs to the worker thread. The main thread's sleep has no timeout. Merely changing the main call to a timed one would change the behavior under investigation, because the bug report explicitly excludes that form. A regression test has to preserve the indefinite wait and the child-process exit that caused the unintended return.

The issue records Yusuke Endoh's bisection to the relevant scheduler change and Luke Gruber's explanation of the correction. It also marks older supported branches as not needing this particular backport and the 4.0 branch as fixed. That history supports calling this an early Ruby 4.0 regression. It does not justify treating every Ruby thread interruption as the same defect.

The major release contains independent experiments

Ruby 4.0.0 introduced Ruby Box and ZJIT in December. Ruby Box isolates definitions and changes such as monkey patches within a box, with experimental status and explicit enablement. ZJIT is a new method-based compiler intended to raise the performance ceiling through larger compilation units and an SSA intermediate representation.

The major release announcement advises experimentation with ZJIT while discouraging production deployment at that stage. It says ZJIT is faster than the interpreter but not yet as fast as YJIT. Those qualifications remain relevant to a patch upgrade. Installing Ruby 4.0.1 does not, by itself, supply a new recommendation to enable every experimental facility introduced by 4.0.

The 4.0.1 release record also lists an Array#pack underflow correction and a fix ensuring that a memberless Data object is frozen. These concern separate runtime contracts. The patch collects corrections across the language without requiring one overarching feature to explain them.

My interest in this release is therefore the restoration of ordinary behavior. The sleep fix shows how concurrency regressions can arise from bookkeeping around an otherwise legitimate scheduler change. The January announcement also sets out an intended two-month maintenance cadence, while allowing earlier releases for changes that significantly affect users. For code already adopting Ruby 4.0, this patch supplies the corrected indefinite-sleep behavior without making experimental JIT adoption part of the decision.