Ruby & Rails

The garbage you never had to create

Ruby's collector is fast. The cheapest object is still the one you didn't allocate, and in hot paths that difference is measurable.

4 February 2026 2 min read Mohammad Aaquib Jawed

Most Ruby code should not think about allocation. In a hot path — a loop over a large collection, a serialiser, a parser — it becomes the dominant cost, and it shows up as diffuse GC time rather than an obvious hotspot.

Where the garbage comes from

Every string literal in Ruby creates a new object each time it is evaluated. In a method called a million times, that is a million strings that exist only to be discarded.

The frozen_string_literal: true magic comment makes literals in that file frozen and shared. It is a one-line change per file, and on string-heavy code the reduction in allocations is substantial. Ruby has been moving toward this being the default, so adopting it also future-proofs the file.

The caveat is that mutating a frozen string raises. Code doing str << "more" on a literal will break, which is exactly the code that most needed changing.

Building strings without building garbage

Concatenation with + creates a new string per operation. In a loop that is quadratic in garbage.

Appending with << mutates in place and creates nothing. For assembling output, that difference is the whole game. Interpolation is fine — it builds one string — but repeated interpolation inside a loop is not.

Enumerable chains and intermediate arrays

Each of map, select and reject returns a new array. A three-method chain over a large collection holds three full copies.

lazy avoids the intermediates by passing elements through one at a time. each_with_object avoids them by building one result. filter_map does select and map in a single pass, which is the common case and one array instead of two.

The fastest object is the one you never allocated, and the second fastest is the one you allocated once.

Symbols, hashes and small wins that add up

Symbols are not garbage collected in the way strings are and are the right key type for known, fixed sets. Dynamically creating symbols from user input is the opposite — that is unbounded growth.

Hash lookups with string keys allocate when the key is built per call. Frozen constants for repeated keys remove that.

And each instead of map when you are ignoring the return value costs nothing and saves the array.

Measure, do not assume

All of this is worth doing in the small number of places that run constantly, and worth ignoring everywhere else. Micro-optimising a controller action that runs once per request is wasted effort and makes the code worse.

An allocation profiler tells you which lines create the most objects. That list is usually short, frequently surprising, and it is the only sensible place to start.

All writing Reply by email