35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails Caching: Wat de Tutorials Je Niet Vertellen

Rails Caching: Wat de Tutorials Je Niet Vertellen

TTB Software
rails, performance, devops
Fragment caching, Russian Doll patterns, cache invalidatie die echt werkt, en de debugging trucs die ik heb geleerd van productie-incidenten.

Elke Rails caching tutorial laat je hetzelfde zien: wikkel wat HTML in een cache block, kijk hoe je response times dalen. Simpel. Schoon. En ongeveer 20% van wat je daadwerkelijk moet weten wanneer het serieus wordt.

Ik heb de afgelopen tien jaar caching-problemen in Rails applicaties opgelost. Het patroon herhaalt zich: een team voegt caching toe, performance verbetert, en dan zes maanden later zitten ze te debuggen waarom er oude data verschijnt of waarom hun Redis-geheugen maar blijft groeien. Dit is wat ik eerder had willen weten.

De Fragment Cache Valkuil

Fragment caching ziet er onschuldig uit:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

Rails genereert een cache key van de cache_key_with_version van het product, inclusief de updated_at timestamp. Update het product, cache invalideert automatisch. Prachtig.

Totdat iemand een prijsregel toevoegt die afhangt van het lidmaatschapsniveau van de huidige gebruiker. Of totdat een gerelateerd object verandert. Of totdat je beseft dat updated_at niet verandert als associaties veranderen.

Het probleem is niet fragment caching zelf. Het probleem is aannemen dat updated_at alle redenen dekt waarom een cached fragment zou moeten veranderen.

Cache Keys Bouwen Die Echt Werken

Hier is een cache key patroon dat ik constant gebruik:

# app/helpers/cache_key_helper.rb
module CacheKeyHelper
  def product_cache_key(product, user)
    [
      product,
      product.category,
      user&.membership_tier,
      PricingRule.maximum(:updated_at),
      "v2" # verhoog dit wanneer de template verandert
    ]
  end
end
<% cache product_cache_key(product, current_user) do %>
  <%= render product %>
<% end %>

Nu invalideert de cache wanneer:

  • Het product verandert
  • De categorie van het product verandert
  • Het niveau van de gebruiker verandert (andere gebruiker = andere cache entry)
  • Een prijsregel verandert
  • Je een template-wijziging deployt en de versie verhoogt

De array wordt automatisch genormaliseerd naar een cache key. Rails handelt dit goed af.

Russian Doll Caching: De Echte Versie

Tutorials leggen Russian Doll caching uit als geneste fragments waarbij binnenste caches de buitenste invalidatie overleven. Klopt. Maar de implementatiedetails doen ertoe.

<% cache ["products-list", @products.maximum(:updated_at), current_user&.id] do %>
  <% @products.each do |product| %>
    <% cache product do %>
      <% cache [product, :details] do %>
        <%= product.description %>
        <%= render product.specifications %>
      <% end %>
      <% cache [product, :pricing, current_user&.membership_tier] do %>
        <%= render partial: "pricing", locals: { product: product } %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

Wanneer een product update, wordt alleen dat product’s fragment opnieuw gebouwd. De buitenste lijst-cache invalideert nog steeds (omdat maximum(:updated_at) veranderde), maar het herbouwen betekent alleen het opnieuw samenstellen van de binnenste gecachte fragments. Snel.

Maar hier gaat het vaak mis: als je de structuur van binnenste fragments verandert zonder de cache keys te veranderen, krijg je oude HTML ingebed in verse wrappers. Voeg altijd een versie toe aan cache keys wanneer je templatestructuur aanpast.

Low-Level Caching voor Dure Berekeningen

Fragment caching handelt view rendering af. Low-level caching handelt al het andere af:

class Product < ApplicationRecord
  def recommended_products
    Rails.cache.fetch([self, :recommendations, Recommendation.maximum(:updated_at)], expires_in: 1.hour) do
      Recommendation.compute_for(self) # dure ML call of complexe query
    end
  end
end

De cache key bevat het product zelf en de laatste recommendation update. De expires_in werkt als veiligheidsklep—zelfs als de key-logica een invalidatie-trigger mist, ververst de data binnen een uur.

Voor echt dure berekeningen voeg ik vaak race condition bescherming toe:

Rails.cache.fetch(cache_key, expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  expensive_computation
end

Dit voorkomt cache stampedes waarbij meerdere processen tegelijk proberen een verlopen waarde opnieuw te berekenen.

Cache Invalidatie: Het Moeilijke Probleem

De quote van Phil Karlton over cache invalidatie als een van de twee moeilijke problemen in computer science wordt zo vaak herhaald dat mensen vergeten het daadwerkelijk op te lossen.

Dit zijn de strategieën die werken:

Touch propagation:

class LineItem < ApplicationRecord
  belongs_to :order, touch: true
end

class Order < ApplicationRecord
  belongs_to :customer, touch: true
end

Een line item updaten toucht de order, die de customer toucht. Cache keys gebaseerd op updated_at invalideren door de keten heen.

Maar touch: true kan slecht cascaderen. Ik heb saves gezien die honderden records touchten, elk met callbacks en validaties. Wees bewust van je touch-graaf.

Versie-kolommen:

class Product < ApplicationRecord
  belongs_to :category

  def cache_version
    [updated_at, category.updated_at, inventory_version].max
  end

  def increment_inventory_version!
    update_column(:inventory_version, inventory_version + 1)
  end
end

Aparte versie-tracking voor verschillende belangen. Prijswijzigingen, voorraad-updates en content-edits moeten mogelijk verschillende caches invalideren.

Expliciete invalidatie:

class InventoryService
  def update_stock(product, quantity)
    product.update!(stock: quantity)
    Rails.cache.delete_matched("products/#{product.id}/*")
    Rails.cache.delete_matched("categories/#{product.category_id}/*")
  end
end

Soms is expliciet beter dan slim. delete_matched is traag op grote cache stores, maar voor gerichte invalidatie is het prima.

Cache Problemen Debuggen

Wanneer iets er stale uitziet maar dat niet zou moeten zijn:

# Check wat er daadwerkelijk in de cache zit
key = controller.fragment_cache_key([:product, @product])
Rails.cache.exist?(key) # Is het gecacht?
Rails.cache.read(key)   # Wat is gecacht?

# In Rails console
ActiveSupport::Cache::Store.logger = Logger.new(STDOUT)
# Nu worden alle cache operaties gelogd

Voor fragment caches specifiek:

<% cache product do %>
  <!-- DEBUG: <%= product.cache_key_with_version %> -->
  <%= render product %>
<% end %>

Die HTML comment toont precies welke cache key gebruikt wordt. Check of het matcht met wat je verwacht.

Redis Geheugen en Cache Eviction

Standaard gebruikt Rails met Redis welke eviction policy je Redis geconfigureerd heeft (vaak noeviction). Dit kan writes laten falen wanneer Redis vol raakt.

Configureer Redis met maxmemory-policy allkeys-lru voor cache workloads. Least-recently-used eviction houdt de hot data.

Maar ook: stel redelijke expires_in waarden in. Gecachte data zonder expiratie accumuleert voor altijd:

# config/initializers/cache.rb
Rails.application.config.cache_store = :redis_cache_store, {
  url: ENV['REDIS_URL'],
  expires_in: 1.day # standaard expiratie voor alle keys
}

Voor fragment caches kun je niet direct expiratie instellen, maar je kunt een cache store gebruiken die het ondersteunt:

config.action_controller.cache_store = :redis_cache_store, {
  expires_in: 12.hours
}

Wanneer Caching Meer Kwaad Dan Goed Doet

Niet alles moet gecacht worden. Vuistregels:

Sla caching over wanneer:

  • De data vaker verandert dan dat het gelezen wordt
  • Cache key berekening de query-tijd benadert
  • De gecachte content sterk gepersonaliseerd is (je cachet per gebruiker, cache hit rate keldert)
  • De onderliggende query al snel is en de view simpel

Investeer in caching wanneer:

  • Dezelfde data herhaaldelijk opgevraagd wordt
  • Berekening of rendering echt duur is
  • Cache keys simpel en betrouwbaar kunnen zijn
  • Je monitoring hebt om stale data problemen te detecteren

De beste caching is saaie caching. Voorspelbare keys, expliciete invalidatie, verstandige expiratie. De slimme aanpakken worden meestal debugging-nachtmerries.

Productie-lessen

Een paar dingen die ik op de harde manier heb geleerd:

Warm de cache op bij deploy. Wanneer je deployt en cache keys veranderen (nieuwe asset fingerprints, versie-bumps), is je cache koud. Accepteer de tijdelijke vertraging of draai een cache warming job:

# lib/tasks/cache.rake
task warm: :environment do
  Product.find_each do |product|
    ProductSerializer.new(product).to_json # raakt de cache
  end
end

Monitor cache hit rates. Zet gestructureerde logging op om cache-prestaties in productie te volgen. Een cache met 10% hit rate is slechter dan geen cache—je betaalt de read overhead op elk request plus de write overhead soms. Volg dit.

Test cache invalidatie expliciet. Niet alleen “werkt de cache” maar “wanneer ik dit verander, invalideert de cache daadwerkelijk.” Schrijf tests voor je invalidatie-logica.

Als je cache warming jobs draait bij deploy, zijn die ideaal voor Solid Queue of Sidekiq — ze kunnen asynchroon draaien zonder de deploy zelf te vertragen.

Caching in Rails is niet moeilijk. Caching correct doen vergt oefening en aandacht. Het goede nieuws: zodra je de patronen begrijpt, zijn ze overdraagbaar naar elk project.

Veelgestelde Vragen

Wanneer moet ik Redis of Memcached gebruiken voor Rails caching?

Redis is de betere standaardkeuze voor de meeste Rails-apps. Het ondersteunt datastructuren voorbij simpele key-value pairs, behoudt data na herstarts, en je draait het waarschijnlijk al voor background jobs of Action Cable. Memcached is eenvoudiger en iets sneller voor pure cache-workloads, maar Redis’ veelzijdigheid wint meestal.

Hoe debug ik verouderde cache-data in productie?

Begin met de cache key controleren — voeg een HTML-comment toe met cache_key_with_version om te bevestigen dat de key overeenkomt met wat je verwacht. Verifieer vervolgens of de cache-entry bestaat met Rails.cache.exist?(key) en lees hem met Rails.cache.read(key). Schakel tijdelijk cache-logging in met ActiveSupport::Cache::Store.logger = Logger.new(STDOUT) in een console-sessie.

Werkt Russian Doll caching met Turbo/Hotwire?

Ja. Russian Doll caching werkt op fragment-niveau tijdens server-side rendering, wat plaatsvindt voordat Turbo de response verwerkt. De gecachte fragments produceren dezelfde HTML ongeacht of het als volledige pagina of als Turbo Frame wordt geleverd. De twee technologieën vullen elkaar goed aan.

Hoe voorkom ik cache stampedes wanneer een populaire cache key verloopt?

Gebruik race_condition_ttl in je Rails.cache.fetch aanroep. Dit verlengt de verlopen entry kort zodat slechts één proces de waarde herberekent terwijl anderen de oude versie blijven serveren. Stel het in op 10-30 seconden voor de meeste workloads.

T

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation