From: "matteo.folin@... (Matteo Folin) via ruby-core" Date: 2026-10-19T14:57:24+00:00 Subject: [ruby-core:127173] [Ruby Bug#22454] Heaps of exited Ractors are reclaimed only by a global GC, so memory grows without bound with short-lived Ractors Issue #22454 has been updated by matteo.folin@gmail.com (Matteo Folin). Subject changed from exited Ractors' memory is reclaimed only by a global GC, so memory grows without bound. to Heaps of exited Ractors are reclaimed only by a global GC, so memory grows without bound with short-lived Ractors ruby -v changed from 4.1.0-preview1 to ruby 4.1.0preview1 (2026-10-11 master 2d08b5f42f) ### Description With the per-Ractor GC in 4.1.0-preview1, a program that keeps creating short-lived Ractors grows its RSS by ~140 MB per 1,000 exited Ractors. As I understand the NEWS, a dead Ractor's heap is reclaimed by a global GC, but nothing starts one when Ractors exit, so the heaps pile up. In 8,000 Ractors only one global GC ran, and memory kept growing after it. Calling `GC.start` after each round of 1,000 keeps RSS flat, so the memory is reclaimable. The automatic trigger doesn't seem to account for it. A server that runs a Ractor per connection hits this directly: it ran out of memory (killed by the kernel's OOM killer in an 8 GB container) a few seconds after its first few thousand connections closed. On 4.0 the same server stays flat. ### Reproduction ```ruby # Rounds of 1,000 short-lived Ractors, each allocating a little and exiting. # Pass "gc" to run GC.start after each round. Warning[:experimental] = false def rss_mb = File.read("/proc/self/status")[/VmRSS:\s+(\d+)/, 1].to_i / 1024 def gc_stat = (GC.stat(scope: :global) rescue GC.stat) 8.times do |round| Array.new(1000) { Ractor.new { Array.new(900) { "x" * 30 }.size } }.each(&:join) GC.start if ARGV[0] == "gc" stat = gc_stat puts "round #{round}: rss #{rss_mb} MB, Ractor.count #{Ractor.count}, " \ "GCs #{stat[:count]}, global GCs #{stat.fetch(:global_gc_count, "-")}" end ``` ### Results Linux aarch64 (Docker on macOS, official `ruby` images), pinned to 4 cores. 4.1.0-preview1, `ruby dead_ractor_heaps.rb`: ``` round 0: rss 159 MB, Ractor.count 1, GCs 1000, global GCs 0 round 1: rss 302 MB, Ractor.count 1, GCs 2000, global GCs 0 round 2: rss 448 MB, Ractor.count 1, GCs 3000, global GCs 0 round 3: rss 596 MB, Ractor.count 1, GCs 4000, global GCs 0 round 4: rss 733 MB, Ractor.count 1, GCs 5001, global GCs 1 round 5: rss 860 MB, Ractor.count 1, GCs 6001, global GCs 1 round 6: rss 988 MB, Ractor.count 1, GCs 7001, global GCs 1 round 7: rss 1125 MB, Ractor.count 1, GCs 8001, global GCs 1 ``` 4.1.0-preview1, `ruby dead_ractor_heaps.rb gc`: ``` round 0: rss 162 MB, Ractor.count 1, GCs 1001, global GCs 1 round 1: rss 292 MB, Ractor.count 1, GCs 2002, global GCs 2 round 2: rss 435 MB, Ractor.count 1, GCs 3003, global GCs 3 round 3: rss 435 MB, Ractor.count 1, GCs 4004, global GCs 4 ... round 7: rss 436 MB, Ractor.count 1, GCs 8008, global GCs 8 ``` 4.0.7, either way: RSS 34 ��� 39 MB over the 8 rounds (44 MB with `gc`). ### Expected RSS bounded without an explicit `GC.start`, for example a global GC (or reclaiming dead heaps without one) triggered by the memory or the count of exited Ractors' heaps waiting to be reclaimed. Also worth a look: even with `GC.start` every round, RSS levels off at ~435 MB, against ~44 MB on 4.0.7. ---------------------------------------- Bug #22454: Heaps of exited Ractors are reclaimed only by a global GC, so memory grows without bound with short-lived Ractors https://bugs.ruby-lang.org/issues/22454#change-119588 * Author: matteo.folin@gmail.com (Matteo Folin) * Status: Open * ruby -v: ruby 4.1.0preview1 (2026-10-11 master 2d08b5f42f) * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- - 8 rounds of 1,000 short-lived Ractors on 4.1.0-preview1: RSS climbs from 159 MB to 1,125 MB, with a single global GC. - With a GC.start after each round, it stops at ~435 MB. - On 4.0.7 it stays at 34���44 MB. - It tells the server story: OOM-killed in 8 GB once the first sockets closed. - It asks for a trigger based on the exited Ractors still waiting to be reclaimed, and notes that even 435 MB is ~10�� 4.0's figure. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/