Frozen String Literals and Immutability

· 4 min read · Updated April 5, 2026 · intermediate
strings frozen immutability performance memory

Ruby strings are mutable by default. You can call << to append, gsub! to replace, or encode to transform them in place. This mutability is convenient, but it comes with a hidden cost: every time you write a string literal, Ruby allocates a new object. If your program creates thousands of identical string literals, that’s thousands of separate objects the garbage collector has to track.

The frozen_string_literal pragma addresses this — and introduces immutability as a side effect. Understanding when and why to use it will make your Ruby code faster and your intent clearer.

What the Pragma Does

Place # frozen_string_literal: true at the very top of a Ruby source file:

# frozen_string_literal: true

class Greeting
  def initialize(name)
    @name = name
  end

  def hello
    "Hello, #{@name}!"
  end
end

From that point in the file, all string literals are frozen — they become immutable String objects. If any code tries to modify one, Ruby raises a FrozenError:

# frozen_string_literal: true

greeting = "hello"
greeting << " world"   # FrozenError: can't modify frozen String

Why Frozen Strings Improve Performance

Without the pragma, Ruby allocates a new String object each time it encounters a literal. Even if two literals contain the same text, they’re separate objects with different object IDs:

# without frozen_string_literal
3.times do
  puts "hello".object_id
end
# prints three different numbers

With the pragma, Ruby can reuse the same frozen object across the file, since immutable objects are safe to share:

# frozen_string_literal: true
3.times do
  puts "hello".object_id
end
# prints the same number three times

Fewer allocations means less work for the garbage collector. In applications that create large numbers of strings — think template-heavy web apps or data processing scripts — this can add up to measurable performance gains.

What Gets Frozen — and What Doesn’t

The pragma freezes only static string literals — the things in quotes. Everything else stays mutable:

# frozen_string_literal: true

# These are frozen:
"hello"
'hello'
"hello #{10}"   # interpolating a literal number is still static, thus frozen

# These are NOT frozen:
"hello #{name}"       # dynamic — depends on runtime value
string = "hello"      # the variable, not the literal
string.upcase         # returns a new string (not the original)

In Ruby 3.0+, interpolated strings are always mutable, even with the pragma. Only the non-interpolated parts are frozen.

The FrozenError Exception

When Ruby 2.x encountered an attempt to modify a frozen string, it raised a plain RuntimeError. Starting with Ruby 3.0, this is a dedicated FrozenError:

# frozen_string_literal: true

s = "hello"
s << " world"
# FrozenError (Ruby 3.0+)
# RuntimeError (Ruby 2.x)

If you need to handle frozen string mutations, rescue FrozenError:

begin
  s << " world"
rescue FrozenError
  s = s + " world"   # create a new mutable string instead
end

Mutating a Frozen String

When you genuinely need a mutable version, String#dup or String#clone creates a copy:

# frozen_string_literal: true

original = "hello"
copy = original.dup   # copy is mutable

copy << " world"
puts copy    # "hello world"
puts original  # "hello" — unchanged

Freezing Objects Explicitly with freeze

The pragma only covers string literals in the file where it appears. For any other object — or for strings you want to freeze at runtime — call freeze directly:

config = {
  api_key: ENV["API_KEY"],
  endpoint: "https://api.example.com",
}.freeze

config[:api_key] = "new"   # FrozenError

freeze works on any object, not just strings:

array = [1, 2, 3].freeze
array << 4   # FrozenError

freeze is shallow — it freezes the object itself but not any objects it contains:

frozen = [[1, 2], [3, 4]].freeze
frozen[0] << 99   # no error — inner arrays are not frozen
frozen << [5, 6]  # FrozenError — the array itself is frozen

For deep freezing, you need to recursively freeze nested structures or use a gem like deep_freeze.

Enabling Globally

Instead of adding the pragma to every file, you can enable it project-wide:

ruby --enable-frozen-string-literal your_script.rb

Many Ruby’s performance guides recommend enabling it globally in production, since the memory and GC benefits apply to all strings throughout the application.

Constants and Immutability

A related concern is constant immutability. By default, constants holding mutable objects can be modified:

MAX_USERS = [100, 200, 500]   # array is mutable
MAX_USERS << 1000             # no error

You can use the shareable_constant_value pragma to automatically freeze constant values:

# shareable_constant_value: literal

MAX_USERS = [100, 200, 500]   # array is deeply frozen
MAX_USERS << 1000             # FrozenError

When to Use the Pragma

Use # frozen_string_literal: true in:

  • All new Ruby files you write — it’s a good default habit
  • Performance-critical code with many string literals
  • Code you want to publish as gems (it was introduced to help gems prepare for Ruby 3.0)

The pragma has no real downside in most cases — if you’re not modifying string literals, making them frozen costs you nothing. The GC and allocation benefits are free immutability.

See Also