Rails Console Script - Watch memcache status for Rails App

Here is a simple script I use to watch the memcached status for a Rails app (the unix.com man-pages Rails app), from the Rails console:

Rails console:

irb(main):019* loop do
irb(main):020*   s = Rails.cache.stats.values.first
irb(main):021* 
irb(main):022*   used_mb  = (s["bytes"].to_i / 1024.0 / 1024.0).round(2)
irb(main):023*   items    = s["curr_items"].to_i
irb(main):024*   hits     = s["get_hits"].to_i
irb(main):025*   misses   = s["get_misses"].to_i
irb(main):026* 
irb(main):027*   # Evictions can appear under different field names depending on memcached build
irb(main):028*   evicts = s["evictions"] || s["evicted_unfetched"] || s["reclaimed"] || 0
irb(main):029* 
irb(main):030*   hitrate = hits + misses > 0 ? (hits.to_f / (hits + misses) * 100).round(2) : 0
irb(main):031* 
irb(main):032*   puts "used: #{used_mb} MB | items: #{items} | hit-rate: #{hitrate}% | hits: #{hits} | misses: #{misses} | evicts: #{evicts}"
irb(main):033* 
irb(main):034*   sleep 2
irb(main):035> end

Example Output:

used: 474.56 MB | items: 372004 | hit-rate: 40.3% | hits: 101411 | misses: 150238 | evicts: 0
used: 474.54 MB | items: 372001 | hit-rate: 40.3% | hits: 101416 | misses: 150242 | evicts: 0
used: 474.54 MB | items: 372002 | hit-rate: 40.3% | hits: 101423 | misses: 150251 | evicts: 0
used: 474.53 MB | items: 372001 | hit-rate: 40.3% | hits: 101424 | misses: 150257 | evicts: 0
used: 474.55 MB | items: 371993 | hit-rate: 40.3% | hits: 101427 | misses: 150262 | evicts: 0
used: 474.54 MB | items: 371990 | hit-rate: 40.3% | hits: 101429 | misses: 150270 | evicts: 0
used: 474.54 MB | items: 371990 | hit-rate: 40.3% | hits: 101434 | misses: 150274 | evicts: 0
used: 474.52 MB | items: 371985 | hit-rate: 40.3% | hits: 101434 | misses: 150281 | evicts: 0
used: 474.52 MB | items: 371985 | hit-rate: 40.3% | hits: 101436 | misses: 150290 | evicts: 0
used: 474.58 MB | items: 371989 | hit-rate: 40.3% | hits: 101440 | misses: 150298 | evicts: 0
used: 474.65 MB | items: 371991 | hit-rate: 40.3% | hits: 101443 | misses: 150307 | evicts: 0
used: 474.68 MB | items: 371994 | hit-rate: 40.29% | hits: 101445 | misses: 150315 | evicts: 0
used: 474.73 MB | items: 372002 | hit-rate: 40.29% | hits: 101448 | misses: 150325 | evicts: 0
used: 474.73 MB | items: 372005 | hit-rate: 40.29% | hits: 101450 | misses: 150328 | evicts: 0
used: 474.72 MB | items: 372002 | hit-rate: 40.3% | hits: 101459 | misses: 150331 | evicts: 0

Notes:

The Rails memcached cache requires the 'dalli` gem:

Example Usage:

os = params[:os].downcase
section =  params[:section].downcase
query = params[:query].downcase
 
cache_key = "man_page:#{os}:#{section}:#{query}"

@man_page = Rails.cache.fetch(cache_key, expires_in: 7.days) do
  ManPage.find_by(os: os, section: section, query: query)
end

Here is the Rails rake task we created (ChatGPT 5.1 and I) to warm the cache:

Rake Task

require "shellwords"

namespace :cache do
  desc "Warm Rails cache by curling all man-page URLs on port 3001"
  task warm: :environment do
    puts "[cache:warm] Starting cache warm-up..."
    STDOUT.flush

    base  = "http://127.0.0.1:3001/man_page"
    total = ManPage.count

    if total.zero?
      puts "[cache:warm] ManPage.count == 0 — nothing to warm. Check RAILS_ENV/DB."
      next
    end

    puts "[cache:warm] Total records to warm: #{total}"
    STDOUT.flush

    count  = 0
    start  = Time.now
    barlen = 20

    def color(c); "\e[#{c}m"; end
    RESET = "\e[0m"

    ManPage.find_each(batch_size: 500) do |mp|
      count += 1

      url = "#{base}/#{mp.os}/#{mp.section}/#{mp.query}/"
      system("curl -s #{Shellwords.escape(url)} > /dev/null")

      # Progress every 1000 rows
      if count % 1000 == 0
        pct    = (count.to_f / total)
        filled = (pct * barlen).to_i
        bar    = ("█" * filled) + ("░" * (barlen - filled))
        pct_text = (pct * 100).round(2)

        elapsed = Time.now - start
        rate    = (count / elapsed rescue 0.0)
        remain  = total - count
        eta_sec = (remain / rate rescue 0.0)

        eta_h = (eta_sec / 3600).to_i
        eta_m = ((eta_sec % 3600) / 60).to_i

        puts "#{color(32)}[#{bar}]#{RESET} #{pct_text}% | " \
             "#{rate.round(1)} req/s | ETA #{eta_h}h #{eta_m}m " \
             "(#{count}/#{total})"
        STDOUT.flush
      end

      sleep 0.01
    end

    total_time = Time.now - start
    puts "[cache:warm] Cache warm-up completed in #{total_time.round(1)}s"
    STDOUT.flush
  end
end

Example Terminal Output

Postscript

I don’t think human society is heading in a good direction with AIs and LLMs, but I can’t deny that they function as remarkably effective assistants—fast, efficient, and capable of collapsing hours of work into minutes. The problem is that this convenience isn’t free. Every gain in speed or automation comes with a human cost: erosion of individual expertise, increased dependency on systems no one fully understands, displacement of people whose work becomes “algorithmically replaceable,” and a subtle flattening of thought as large models standardize the way problems are framed and solved.

We get more efficiency, yes—but at the price of narrowing human roles and shifting more control to systems that shape behavior, expectations, and even intellectual habits. That trade-off is rarely acknowledged honestly, and that’s the part that concerns me: society is sprinting toward maximal convenience without asking whether the cost is worth paying.

For a very good science fiction take on this, I highly recommend "Service Model" by Adrian Tchaikovsk:

Final version of this warming script after small tweaks:

Rake Task: Warm Man Pages Memcache

require "shellwords"

namespace :cache do
  desc "Warm cache with man pages ordered by hits, fast and without heavy ORDER BY per batch"
  task warm: :environment do
    puts "[cache:warm] Starting cache warm-up..."
    STDOUT.flush

    base  = "http://127.0.0.1:3001/man_page"
        
    # Preload ALL IDs sorted by hits descending — one query only
    ids = ManPage.order(hits: :desc).pluck(:manid)
    total = ids.length

    puts "[cache:warm] Total records to warm: #{total}"
    STDOUT.flush
        
    count  = 0
    start  = Time.now
    barlen = 20
        
    def color(c); "\e[#{c}m"; end
    RESET = "\e[0m"
        
    ids.each do |id|
      mp = ManPage.find(id)
      
      count += 1
    
      url = "#{base}/#{mp.os}/#{mp.section}/#{mp.query}/"
      system("curl -s #{Shellwords.escape(url)} > /dev/null")
           if count % 1000 == 0
        pct    = count.to_f / total
        filled = (pct * barlen).to_i
        bar    = ("█" * filled) + ("░" * (barlen - filled))
        pct_text = (pct * 100).round(2)

        elapsed = Time.now - start
        rate    = (count / elapsed rescue 0.0)
        remain  = total - count
        eta_sec = (remain / rate rescue 0.0)

        eta_h = (eta_sec / 3600).to_i
        eta_m = ((eta_sec % 3600) / 60).to_i

        puts "#{color(32)}[#{bar}]#{RESET} #{pct_text}% | #{rate.round(1)} req/s | ETA #{eta_h}h #{eta_m}m (#{count}/#{total})"
        STDOUT.flush
      end

      sleep 0.01
    end

    total_time = Time.now - start
    puts "[cache:warm] Completed in #{total_time.round(1)}s"
    # Reset memcached stats after warm completes
    system('echo "stats reset" | nc -w 1 127.0.0.1 11211')
    puts "[cache:warm] Memcached stats reset."
    STDOUT.flush
  end
end

Hit Rate after Caches Warmed and Stats Cleared:

used: 1093.28 MB | items: 352595 | hit-rate: 88.51% | hits: 3943 | misses: 512 | evicts: 0

used: 1093.28 MB | items: 352608 | hit-rate: 88.46% | hits: 4025 | misses: 525 | evicts: 0

used: 1093.28 MB | items: 352624 | hit-rate: 88.37% | hits: 4111 | misses: 541 | evicts: 0

used: 1093.29 MB | items: 352644 | hit-rate: 88.19% | hits: 4190 | misses: 561 | evicts: 0

used: 1093.29 MB | items: 352654 | hit-rate: 88.21% | hits: 4272 | misses: 571 | evicts: 0

used: 1093.29 MB | items: 352661 | hit-rate: 88.27% | hits: 4349 | misses: 578 | evicts: 0

used: 1093.29 MB | items: 352673 | hit-rate: 88.25% | hits: 4433 | misses: 590 | evicts: 0

used: 1093.29 MB | items: 352684 | hit-rate: 88.25% | hits: 4514 | misses: 601 | evicts: 0

used: 1093.3 MB | items: 352698 | hit-rate: 88.23% | hits: 4612 | misses: 615 | evicts: 0

used: 1093.3 MB | items: 352705 | hit-rate: 88.32% | hits: 4703 | misses: 622 | evicts: 0

used: 1093.3 MB | items: 352714 | hit-rate: 88.34% | hits: 4780 | misses: 631 | evicts: 0

used: 1093.3 MB | items: 352723 | hit-rate: 88.38% | hits: 4867 | misses: 640 | evicts: 0

used: 1093.3 MB | items: 352730 | hit-rate: 88.45% | hits: 4953 | misses: 647 | evicts: 0

used: 1093.31 MB | items: 352745 | hit-rate: 88.39% | hits: 5040 | misses: 662 | evicts: 0

used: 1093.31 MB | items: 352759 | hit-rate: 88.34% | hits: 5123 | misses: 676 | evicts: 0

used: 1093.31 MB | items: 352770 | hit-rate: 88.35% | hits: 5209 | misses: 687 | evicts: 0

used: 1093.31 MB | items: 352780 | hit-rate: 88.36% | hits: 5290 | misses: 697 | evicts: 0

used: 1093.31 MB | items: 352788 | hit-rate: 88.39% | hits: 5368 | misses: 705 | evicts: 0

88 - 89 Percent Cache Hit Rate

Notes:

Memcached has no persistence across restarts, and there’s no practical way to dump and restore a full memcached cache snapshot. Because of this limitation, switching to Redis makes sense. Redis supports on-disk persistence (RDB or AOF), so we can restart the cache server without losing the warmed dataset — avoiding the need to re-run a multi-hour cache warm every time (with memcached).

Switch from memcached to redis done.

Now, we can stop and start redis, saving the cache snapshot to disk, not losing the cache on restarts.

Restarted Cache - 91% Hit Rate

used: 1218.17 MB | items: 342000 | hit-rate: 91.13% | hits: 5273 | misses: 513
used: 1218.14 MB | items: 342013 | hit-rate: 91.17% | hits: 5429 | misses: 526
used: 1218.16 MB | items: 342023 | hit-rate: 91.19% | hits: 5545 | misses: 536
used: 1218.17 MB | items: 342040 | hit-rate: 91.13% | hits: 5682 | misses: 553
used: 1218.17 MB | items: 342051 | hit-rate: 91.18% | hits: 5830 | misses: 564
used: 1218.15 MB | items: 342059 | hit-rate: 91.25% | hits: 5962 | misses: 572
used: 1218.18 MB | items: 342072 | hit-rate: 91.24% | hits: 6091 | misses: 585
used: 1218.18 MB | items: 342091 | hit-rate: 91.18% | hits: 6247 | misses: 604
used: 1218.16 MB | items: 342106 | hit-rate: 91.19% | hits: 6405 | misses: 619
used: 1218.19 MB | items: 342121 | hit-rate: 91.13% | hits: 6503 | misses: 633
used: 1218.16 MB | items: 342134 | hit-rate: 91.07% | hits: 6599 | misses: 647
used: 1218.17 MB | items: 342142 | hit-rate: 91.1% | hits: 6702 | misses: 655
used: 1218.21 MB | items: 342152 | hit-rate: 91.1% | hits: 6805 | misses: 665
used: 1218.15 MB | items: 342168 | hit-rate: 91.03% | hits: 6912 | misses: 681
used: 1218.15 MB | items: 342178 | hit-rate: 91.02% | hits: 7005 | misses: 691
used: 1218.18 MB | items: 342187 | hit-rate: 91.01% | hits: 7088 | misses: 700

Notes

  • ChatGPT 5.1 is a major workflow accelerator — greatly reduced friction on debugging, Rails tuning, caching strategy, server config, and Redis warm-load design.

  • Continued preference for the Skippy the Magnificent persona (Expeditionary Force).“Rex the Dog” (Dogs of War) persona experiment = fail, discarded quickly.

  • Concept: stream screen session output → Redis → Skippy for real-time terminal ingestion.Currently blocked by platform constraints; would require a custom API-driven app.

  • Redis cache warm-snapshot approach is validated:

    • Prewarmed in dev namespace
    • BGSAVE preserved snapshot
    • Production cutover uses the same namespace
    • Restart-safe, instant-hot cache after boot
  • My opinion - Public sentiment of an "AI Bubble" is not based in fact. Future GPT versions will be even more capable and helpful for mundane day-to-day sys admin and coding tasks.

Today I added a simple, exact man-page search feature to the man pages on www.unix.com. It’s intentionally minimalist – no fuzzy matching, no full-text noise – just a direct lookup by man page name.

For a given query, it returns all matching man pages across all OSes, showing:

  • OS

  • Section

  • Man page name

  • Hits (popularity)

This makes it easy to see, at a glance, which operating systems provide a given man page and in which section, and then click straight through to the page you want.

You can try it here with awk as an example:

https://www.unix.com/man_pages/search/awk

Partial Screen Shot

See Also: