How to Use Ruby’s Object#freeze for Thread‑Safe, Immutable Objects
Freeze makes Ruby objects immutable. Learn the exact syntax, a step‑by‑step example, and common pitfalls so you can use it safely in multi‑threaded code.
10 Jul 2026, 08:17 UTC

Why Freeze Matters
In Ruby, objects are mutable by default. When you share an object between threads, accidental mutation can cause race conditions, subtle bugs, or data corruption. The Object#freeze method turns an object into an immutable value: any attempt to change its state raises a RuntimeError. This simple flag lets you share data safely without external synchronization and can improve performance by eliminating needless copies.
How Freeze Works
Calling freeze on an object sets an internal flag in the object’s header. The Ruby interpreter checks this flag in every mutating method (e.g., Array#push, String#gsub!, Hash#[]=). If the flag is set, the method aborts and raises RuntimeError: can't modify frozen .... The flag does not affect read‑only methods or the ability to call any method on the object; it only blocks mutation.
Freezing is shallow: the object’s immediate instance variables are frozen, but nested objects remain mutable unless they are frozen explicitly. For example, a frozen array containing a hash still allows the hash to be modified.
Worked Example: Freezing a Hash for Thread Safety
Suppose you have a configuration hash that should never change after the application starts. You want to expose it to worker threads without locking.
# config.rb
CONFIG = {
timeout: 5,
retries: 3,
endpoints: ["https://api.example.com", "https://backup.example.com"]
}.freeze
# main.rb
require_relative 'config'
# Two threads read from CONFIG concurrently
threads = 2.times.map do
Thread.new do
puts "Thread #{Thread.current.object_id} sees timeout=#{CONFIG[:timeout]}"
end
end
threads.each(&:join)
Because CONFIG is frozen, neither thread can alter it. If one thread attempted to write to CONFIG[:timeout] = 10, Ruby would raise a RuntimeError immediately, preventing the mutation.
Verifying the Freeze
- Run
puts CONFIG.frozen?– it printstrue. - Attempt
CONFIG[:timeout] = 10– you receiveRuntimeError: can't modify frozen Hash. - Check that the original value remains unchanged:
puts CONFIG[:timeout]still outputs5.
Common Mistakes & Limitations
- Shallow Freeze: If your object contains nested mutable structures, you must freeze them separately. Example:
arr = [1, 2, 3] arr.freeze arr[0] = 10 # raises RuntimeError hash = {a: [1, 2]} hash.freeze hash[:a][0] = 10 # still works because the array inside is mutable - Immutable Collections vs. Mutable Elements: Freezing an array does not freeze the objects it holds. A frozen array can still contain mutable hashes.
- Thread Safety Only for Mutation: Freezing removes the need for locks only when reading. If a thread reassigns a variable to a different object, that is a separate issue.
- Not All Objects Can Be Frozen: Ruby 3.x raises
TypeErrorwhen you callfreezeon objects likeIOorProcess. - Memory Footprint: Freezing a large object does not duplicate it, but if you later clone the object, the clone will be a full copy, potentially increasing memory usage.
- Neglecting to Freeze Shared Data: Libraries that expose mutable objects to callers can introduce hidden race conditions if they forget to freeze data that should be immutable.
Practical Checklist Before Using Freeze
- Confirm the object is intended to be immutable after creation.
- Recursively freeze nested objects if they should also be immutable.
- Test that mutation attempts raise
RuntimeErrorbefore deploying to production. - Document the object's frozen status in code comments or documentation.
- Use
frozen?checks in debugging logs to verify state during development.
Conclusion
Ruby’s Object#freeze is a lightweight, built‑in mechanism to enforce immutability. When applied correctly, it eliminates a class of concurrency bugs and can give a small performance boost by allowing the interpreter to skip mutation checks on frozen objects. Remember that freeze is shallow, does not affect variable reassignment, and is not applicable to all object types. With a clear understanding of its semantics and a disciplined approach to freezing shared data, you can write safer, more maintainable Ruby code that scales across threads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.