DEV Community

Cover image for Ruby Hash - String Keys Vs Symbol Keys
kinvoki
kinvoki

Posted on • Originally published at kinvoki.com

Ruby Hash - String Keys Vs Symbol Keys

There was recently a discussion on Trailblazer Gitter channel about Hashes as params, how to pass them around, and as customary a flame-war war insued never happened, and it came down to a measuring contest: whose which key is better and faster.

TLDR;

For small hashes it doesn’t really matter, for larger ones :symbol is 1.15x faster than string’ keys. Frozen string’ keys are in-between.

The best way to argue, is to present facts. So I coded a couple of benchmarks, and submitted a pull request to fast-ruby (Github). Here are the details.

Round 1: Hash[:symbol] vs Hash[“string”]

First lets measure allocating Hash in various ways that Ruby gives us

require "benchmark/ips"

def symbol_key
  {symbol: 42}
end

def symbol_key_arrow
  {:symbol => 42}
end

def symbol_key_in_string_form
  {'sym_str': 42}
end

def string_key_arrow_double_quotes
  {"string" => 42}
end

def string_key_arrow_single_quotes
  {'string' => 42}
end


Benchmark.ips do |x|

  puts "Hash comparison"
  x.report( %({symbol: 42} )){ symbol_key }
  x.report( %({:symbol => 42} )) { symbol_key_arrow }
  x.report( %({'sym_str': 42} )) { symbol_key_in_string_form }
  x.report( %({"string" => 42} )) { string_key_arrow_double_quotes }
  x.report( %({'string' => 42} )) { string_key_arrow_single_quotes }
  x.compare!
end


Enter fullscreen mode Exit fullscreen mode
Hash comparison
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin25]
Warming up --------------------------------------
       {symbol: 42}    807.478k i/100ms
    {:symbol => 42}      1.070M i/100ms
    {'sym_str': 42}      1.083M i/100ms
   {"string" => 42}      1.075M i/100ms
   {'string' => 42}      1.047M i/100ms
Calculating -------------------------------------
       {symbol: 42}      10.776M (± 9.0%) i/s   (92.80 ns/i) -     53.294M in   5.010967s
    {:symbol => 42}      10.921M (± 4.1%) i/s   (91.56 ns/i) -     54.591M in   5.007897s
    {'sym_str': 42}      10.703M (± 9.3%) i/s   (93.44 ns/i) -     53.090M in   5.023091s
   {"string" => 42}      10.543M (± 9.8%) i/s   (94.85 ns/i) -     52.653M in   5.063165s
   {'string' => 42}      10.561M (±10.0%) i/s   (94.69 ns/i) -     52.338M in   5.024881s

Comparison:
    {:symbol => 42} : 10921358.3 i/s
       {symbol: 42} : 10775503.9 i/s - same-ish: difference falls within error
    {'sym_str': 42} : 10702520.9 i/s - same-ish: difference falls within error
   {'string' => 42} : 10560679.5 i/s - same-ish: difference falls within error
   {"string" => 42} : 10543355.5 i/s - same-ish: difference falls within error
Enter fullscreen mode Exit fullscreen mode

Used to be a difference on Ruby 2.*, but running benchmark on Ruby 3.4.7 - we can see that style doesn’t really matter. Using string "42" for values instead of number 42, will slightly change the benchmark, but insignificant in the grand scheme of things.

Don’t overcomplicate

string keys are not going to be the speed bottleneck in your app

Round 2: But what about large hashes?

Don’t worry I got you covered. Lets try it out for a 1000 key-value pairs.

require "benchmark/ips"


STRING_KEYS = (1001..2000).map{|x| "key_#{x}"}.shuffle
FROZEN_KEYS = (2001..3000).map{|x| "key_#{x}".freeze}.shuffle
SYMBOL_KEYS = (3001..4000).map{|x| "key_#{x}".to_sym}.shuffle

# If we use static values for Hash, speed improves even more.
def symbol_hash
  SYMBOL_KEYS.collect { |k| [ k, rand(1..100)]}.to_h
end

def string_hash
  STRING_KEYS.collect { |k| [ k, rand(1..100)]}.to_h
end

# See this article for the discussion of using frozen strings instead of symbols
# http://blog.arkency.com/could-we-drop-symbols-from-ruby/
def frozen_hash
  FROZEN_KEYS.collect { |k| [ k, rand(1..100)]}.to_h
end


SYMBOL_HASH = symbol_hash
STRING_HASH = string_hash
FROZEN_HASH = frozen_hash


def reading_symbol_hash
  SYMBOL_HASH[SYMBOL_KEYS.sample]
end

def reading_string_hash
  STRING_HASH[STRING_KEYS.sample]
end

def reading_frozen_hash
  FROZEN_HASH[FROZEN_KEYS.sample]
end

Benchmark.ips do |x|

  puts "Creating large Hash"
  x.report("Symbol Keys") { symbol_hash }
  x.report("String Keys") { string_hash }
  x.report("Frozen Keys") { frozen_hash }

  x.compare!
end

Benchmark.ips do |x|
  puts "Reading large Hash"
  x.report("Symbol Keys") { reading_symbol_hash }
  x.report("String Keys") { reading_string_hash }
  x.report("Frozen Keys") { reading_frozen_hash }
  x.compare!
end

Enter fullscreen mode Exit fullscreen mode

Let’s see the results:

Creating large Hash
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin25]
Warming up --------------------------------------
         Symbol Keys   816.000 i/100ms
         String Keys   541.000 i/100ms
         Frozen Keys   723.000 i/100ms
Calculating -------------------------------------
         Symbol Keys      7.759k (± 8.5%) i/s  (128.88 μs/i) -     38.352k in   5.005736s
         String Keys      5.716k (± 4.8%) i/s  (174.96 μs/i) -     28.673k in   5.030225s
         Frozen Keys      7.411k (± 8.3%) i/s  (134.93 μs/i) -     36.873k in   5.037710s

Comparison:
         Symbol Keys:     7759.1 i/s
         Frozen Keys:     7411.3 i/s - same-ish: difference falls within error
         String Keys:     5715.6 i/s - 1.36x  slower

Reading large Hash
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin25]
Warming up --------------------------------------
         Symbol Keys     1.373M i/100ms
         String Keys   930.582k i/100ms
         Frozen Keys     1.205M i/100ms
Calculating -------------------------------------
         Symbol Keys     13.467M (± 7.6%) i/s   (74.26 ns/i) -     67.269M in   5.046055s
         String Keys     11.345M (± 1.7%) i/s   (88.14 ns/i) -     56.766M in   5.005134s
         Frozen Keys     11.820M (± 8.8%) i/s   (84.60 ns/i) -     59.025M in   5.068781s

Comparison:
         Symbol Keys: 13466641.7 i/s
         Frozen Keys: 11819675.1 i/s - same-ish: difference falls within error
         String Keys: 11344986.8 i/s - 1.19x  slower
Enter fullscreen mode Exit fullscreen mode

So as we can see on a larger Hashes with with 1000's of keys, the difference become more apparent and being mindful of it can help improve speed, if you are using a lot of large hashes. Otherwise, keep using what works better for you app. If string keys make it easier, let's say you are importing them from an external file, don't go through the trouble of converting them to symbols, it's probably not worth it.

But don't take my word for it. Just measure it.


Last updated: November 1, 2025 - Benchmarks re-run on Ruby 3.4.7 (arm64-darwin25)

Top comments (0)