Techdots

July 22, 2025

Optimizing Performance in Ruby: Garbage Collection, Threading, and Profiling

Optimizing Performance in Ruby: Garbage Collection, Threading, and Profiling

Is your Ruby application running slower than expected? Are you struggling with memory issues or wondering how to handle multiple tasks efficiently?

Ruby is known for its clean, readable code and developer-friendly features. However, when building large applications or handling heavy workloads, performance optimization becomes crucial. Many developers face challenges with slow response times, memory problems, and inefficient task handling.

In this guide, we'll explore practical ways to boost your Ruby application's performance. We'll cover garbage collection tuning, threading versus fibers, choosing between Ruby VMs, and essential profiling tools.ย 

By the end, you'll have actionable strategies to make your Ruby code faster and more efficient.

Understanding Ruby's Memory Management: Garbage Collection

Ruby's memory management system automatically handles memory allocation and cleanup through garbage collection. The MRI (Matz's Ruby Interpreter) uses a generational garbage collector that divides objects into two main groups:

  • Young Generation: New, short-lived objects
  • Old Generation: Long-lived objects that have survived several garbage collection cycles

Understanding how garbage collection works helps you write more memory-efficient code and tune your application for better performance.

How to Tune Garbage Collection

You can optimize garbage collection using environment variables:


RUBY_GC_HEAP_GROWTH_FACTOR=1.5 \
RUBY_GC_HEAP_INIT_SLOTS=600000 \
RUBY_GC_MALLOC_LIMIT=90000000 \
ruby my_app.rb
  

Or check garbage collection statistics programmatically:


GC::Profiler.enable
# Do some allocations
10.times { "a" * 100_000 }
puts GC.stat
puts
puts "GC Profile Report:"
puts GC::Profiler.report
  

When Should You Tune Garbage Collection?

Consider tuning garbage collection when you notice:

  • Long pause times during garbage collection (check with GC::Profiler.report)
  • High memory usage that doesn't decrease over time
  • CPU overhead from frequent minor garbage collections

Threading vs. Fibers: Choosing the Right Concurrency Model

Ruby offers different ways to handle multiple tasks simultaneously. Understanding when to use threading versus fibers can significantly impact your application's performance.

Understanding Ruby's Global Interpreter Lock (GIL)

MRI Ruby has a Global Interpreter Lock (GIL), which means only one thread can execute Ruby code at a time. However, the GIL is released during IO operations, making threading useful for network requests and file operations.

When to Use Threading

Threading works best for:

  • Concurrent IO operations (API calls, file reading)
  • Background job processing
  • Web servers handling multiple requests

Here's a simple threading example:


threads = []
5.times do |i|
  threads << Thread.new do
    puts "Hello from thread #{i}"
  end
end
threads.each(&:join)
  

When to Use Fibers for Lightweight Concurrency

Fibers are more memory-efficient than threads and perfect for cooperative multitasking:


fiber = Fiber.new do
  puts "Fiber running"
  Fiber.yield
  puts "Fiber resumed"
end
fiber.resume
fiber.resume
  

Popular libraries like async and Falcon use fibers to create high-performance Ruby servers.

MRI vs. JRuby: Choosing the Right Ruby VM

While MRI is the standard Ruby implementation, JRuby runs on the Java Virtual Machine and offers different performance characteristics.

MRI Advantages:

  • Large, active community
  • Better gem compatibility
  • Simpler setup and deployment

JRuby Advantages:

  • True parallel threading (no GIL)
  • Access to JVM ecosystem and optimizations
  • Better performance for CPU-intensive tasks

When to Choose JRuby:

  • High-performance concurrent systems
  • Integration with existing Java applications
  • Long-running background processing tasks

Essential Profiling Tools for Ruby Performance

Before optimizing, you need to measure and identify performance bottlenecks. Here are the most effective profiling tools:

1. Benchmark - Simple Performance Measurement


require 'benchmark'
puts Benchmark.measure {
  100_000.times { "string".gsub(/s/, 'z') }
}
  

2. StackProf - Production-Ready Profiling

StackProf is ideal for production environments with minimal overhead:


require 'stackprof'
StackProf.run(mode: :cpu, out: 'tmp/stackprof.dump') do
  # Code you want to profile
  100_000.times do
    "hello".chars.reverse.join
  end
end
  

Analyze the results with:

bash

stackprof tmp/stackprof.dump --text

3. ruby-prof - Detailed Performance Analysis

For comprehensive profiling that tracks method calls and memory allocations:


require 'ruby-prof'
# Start profiling
RubyProf.start
# --- Code to profile ---
100_000.times do
  "ruby".reverse
end
# ------------------------
# Stop profiling
result = RubyProf.stop
# Print flat profile report to STDOUT
printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT)
  

Understanding Ruby's Concurrency Models

Ruby supports multiple approaches to handling concurrent tasks:

Model MRI Support JRuby Support Best Use Case
Threads Yes (with GIL) Yes (true parallelism) IO-bound tasks
Fibers Yes Yes Structured concurrency
Processes Yes (Unix) Limited CPU-bound tasks

Choosing the Right Concurrency Model:

  • Multi-threading: Use for IO-heavy applications like HTTP clients and APIs
  • Multi-processing: Best for CPU-bound or GIL-limited workloads like image processing
  • Fibers: Perfect for scalable, lightweight concurrency in applications like chat servers

Real-World Example

A startup initially used threads for background file compression jobs. As data size grew, performance decreased due to MRI's GIL limitations. They switched to multi-processing using the parallel gem, and CPU-bound tasks like zipping and encrypting large files became approximately 2ร— faster since each process used a separate CPU core.

Performance Optimization Best Practices

Here's a summary of key optimization strategies:

Area Optimization Tip
Garbage Collection Tune via RUBY_GC_* environment variables; minimize short-lived object creation
Threading Use for IO concurrency; avoid for CPU-heavy work in MRI
Fibers Great for coroutine-style code, especially with async gems
Profiling Use stackprof in production, ruby-prof in development
JRuby Use for true parallelism or JVM integration
Concurrency Match your concurrency model to your workload type

Essential Tools and Gems

Here are the key tools for Ruby performance optimization:

  • Stackprof: Fast sampling profiler for production use
  • Ruby-prof: Full-featured profiler for development
  • Benchmark: Built-in benchmarking standard library
  • Async: Fiber-based event loop for async programming
  • Concurrent-ruby: High-level abstractions for threading and fibers

Conclusion

Ruby performance optimization requires understanding your Ruby VM's constraints, using appropriate profiling tools, and selecting the right concurrency model.ย 

Whether you're managing memory usage, finding slow methods, or scaling concurrent workloads, Ruby provides powerful tools when used correctly.

Ready to supercharge your Ruby applications? TechDots can help you implement these performance optimization strategies and build lightning-fast Ruby systems.ย 

Contact us today to get started!

โ€

Ready to Launch Your AI MVP with Techdots?

Techdots has helped 15+ founders transform their visions into market-ready AI products. Each started exactly where you are now - with an idea and the courage to act on it.

Techdots: Where Founder Vision Meets AI Reality

Book Meeting