Rails 8 Solid Cache: Productieopzet, TTL-strategieën en Migreren vanaf Redis
Rails 8 Solid Cache in productie: opzetgids met databasesizing, TTL- en evictiestrategieën, migreren vanaf Redis en monitoren van de cache hit rate.
De Redis-instance van een van mijn klanten kostte $340 per maand op ElastiCache en bevatte ongeveer 900 MB aan cachedata. Toen Rails 8 Solid Cache als standaard cache-backend uitbracht, duurde het gesprek met hun CTO ongeveer vier minuten: “Dus we kunnen het Redis-cluster verwijderen en de cache op de Postgres-database zetten die we toch al draaien?” Ja. En twee weken later ging die ElastiCache-rekening naar nul.
Rails 8 Solid Cache is een database-backed cache store die in dezelfde relationele database leeft die je al gebruikt voor je applicatie. Het vervangt Redis of Memcached voor het merendeel van de Rails-applicaties die ze puur als Rails-cache gebruikten — niet als Sidekiq-queue, niet als Pub/Sub-bus, gewoon als Rails.cache. Als Redis nog zo’n stuk infrastructuur was dat je ops-team onderhield voor wat neerkwam op een fancy key-value tabel, dan kun je het met Solid Cache verwijderen.
Na negentien jaar Rails in productie draaien, deden de meeste Redis-clusters die ik in Rails-apps heb gezien exact dit: fragmenten cachen, dure queries memoizen en sessiedata bewaren. Niks daarvan had sub-milliseconde latency nodig. Niks daarvan had de operationele overhead van een tweede datastore nodig.
Wat Rails 8 Solid Cache Werkelijk Is
Solid Cache is een Rails.cache-implementatie gebaseerd op SQL. Het bewaart elke cache-entry als een rij in een tabel met een byte-array value-kolom, een expiration-timestamp en een hash-based key index. Reads zijn single-row lookups op primary key. Writes zijn inserts met ON CONFLICT DO UPDATE. Expiration en evictie gebeuren via een achtergrond-sweep die binnen je Rails-proces draait — geen cron job, geen aparte worker.
Drie dingen maken het levensvatbaar als Redis-vervanging in plaats van speelgoed:
- Encryptie at rest — cache values kunnen versleuteld worden voor ze op disk komen via
ActiveRecord::Encryption. Sessietokens en gecachte user data zijn beschermd, zelfs als iemand een database-dump te pakken krijgt. - LRU-achtige evictie met size targets — je geeft Solid Cache het maximale aantal rijen of de maximale byte-grootte, en het evicteert de minst recent aangeraakte entries wanneer het target wordt overschreden. Geen ongebreidelde groei.
- Multi-database support — de cache-tabel kan in een andere database staan dan je applicatiedata. In productie zet ik hem altijd op een eigen Postgres-instance of eigen database op een gedeeld cluster.
Het performance-plafond ligt lager dan Redis. Een single-row Postgres lookup met een primary key hit duurt 0,5–2 ms end-to-end vanuit Rails; de equivalente Redis GET duurt 0,2–0,5 ms. Voor fragment caches, view caches en gememoizede query results is het verschil ruis. Voor iets dat atomic increments duizend keer per seconde vereist, gebruik Redis.
Rails 8 Solid Cache Opzetten op een Nieuwe App
Rails 8 komt standaard met Solid Cache geconfigureerd wanneer je een nieuwe app genereert. De solid_cache gem staat in de Gemfile, en config/cache.yml ziet er zo uit:
# config/cache.yml
default: &default
store_options:
max_age: <%= 60.days.to_i %>
max_size: <%= 256.megabytes %>
namespace: <%= Rails.env %>
development:
<<: *default
test:
<<: *default
production:
database: cache
<<: *default
De database: cache regel verwijst naar een entry in config/database.yml. Voeg een aparte cache-database configuratie toe:
# config/database.yml
production:
primary:
<<: *default
database: myapp_production
cache:
<<: *default
database: myapp_production_cache
migrations_paths: db/cache_migrate
Maak het schema aan en draai de Solid Cache migrations:
bin/rails db:create
bin/rails solid_cache:install:migrations
bin/rails db:migrate:cache
Dat is de complete setup. Rails.cache.read("key") en Rails.cache.write("key", value, expires_in: 1.hour) raken nu de Postgres cache-database. Er draait geen Redis-proces.
De Cache-Database Sizen voor Productie
De enige vraag die je Solid Cache-opzet bepaalt is: hoe groot mag de cache-tabel groeien? Als je dit fout hebt, evicteer je constant productieve cache-entries of laat je de cache-tabel opzwellen en je database-backups vertragen.
De formule die ik gebruik voor Rails 8 Solid Cache-sizing is:
max_size = (gemiddelde entry-grootte) × (aantal entries in working set) × 1,3 safety factor
Voor een typische Rails-applicatie met page fragment caching, gecachte query results en Russian-doll caching komt de gemiddelde entry-grootte uit rond 4–12 KB en de working set op 20.000–100.000 live entries. Dat geeft je een cache-grootte tussen 100 MB en 1,5 GB. Bij de klant die ik eerder noemde stelden we max_size: 768.megabytes in en de cache stabiliseerde op 620 MB met een hit rate van 94%.
Het size-target wordt afgedwongen door een achtergrond-trimmer:
# config/cache.yml — volledige productieconfig
production:
database: cache
store_options:
max_age: <%= 30.days.to_i %>
max_size: <%= 768.megabytes %>
namespace: <%= Rails.env %>
expiry_batch_size: 100
expiry_method: :thread
trim_batch_size: 100
encrypt: true
shards:
- cache_primary
- cache_secondary
Twee knoppen zijn hier in de praktijk belangrijk:
expiry_method: :threaddraait de trimmer in een achtergrond-thread binnen elke Puma-worker. Er is geen apart expiry-proces. Op een 4-worker Puma-setup coördineren vier threads via SQL row locks op desolid_cache_entriestabel.expiry_batch_sizeentrim_batch_sizebepalen hoeveel rijen per sweep-pas worden verwijderd. De defaults (100) zijn prima voor cache-tabellen onder 1 GB. Verhoog ze naar 1000 als je cache groter is en expiration achterloopt op writes.
Encryptie voegt 20–30% write-overhead toe en een vergelijkbare read-overhead. Op cache-workloads gedomineerd door hits verschijnt dit als een 5–10% latency-bump in de mediaan. Zet het aan als je iets cachet dat gebruikers identificeert, en zet het nooit lichtvaardig uit — encrypt achteraf omdraaien laat je met onleesbare historische entries zitten.
Migreren van Redis naar Solid Cache
Het migratiepad dat ik gebruik voor bestaande Rails-applicaties op Redis heeft vier fases. Sla er geen enkele over.
Fase 1: Solid Cache naast Redis toevoegen
Installeer de gem, draai migrations, maar houd Redis als config.cache_store in productie. Solid Cache is aanwezig maar inactief:
# config/environments/production.rb
config.cache_store = :redis_cache_store, {
url: ENV["REDIS_URL"],
expires_in: 1.day
}
Ship dit naar productie en verifieer dat migrations schoon zijn gedraaid.
Fase 2: Solid Cache in een shadow-write patroon inschakelen
Wrap Rails.cache in een class die naar zowel Redis als Solid Cache schrijft maar alleen van Redis leest. Zo kun je valideren dat Solid Cache writes ontvangt met de verwachte snelheid zonder gebruikerslatency te beïnvloeden:
# lib/dual_write_cache.rb
class DualWriteCache
def initialize(primary:, shadow:)
@primary = primary
@shadow = shadow
end
def write(key, value, options = nil)
result = @primary.write(key, value, options)
Thread.new { @shadow.write(key, value, options) rescue nil }
result
end
def read(key, options = nil)
@primary.read(key, options)
end
def fetch(key, options = nil, &block)
@primary.fetch(key, options) do
value = block.call
Thread.new { @shadow.write(key, value, options) rescue nil }
value
end
end
# Delegeer alles anders naar primary
def method_missing(method, *args, &block)
@primary.public_send(method, *args, &block)
end
end
Wire het aan in production.rb:
redis_cache = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
solid_cache = ActiveSupport::Cache::SolidCacheStore.new
config.cache_store = DualWriteCache.new(primary: redis_cache, shadow: solid_cache)
Draai dit een week. Kijk hoe de row count van solid_cache_entries groeit. Bevestig dat de dagelijkse write rate overeenkomt met wat je Redis-cluster rapporteert.
Fase 3: Het read-pad omdraaien
Wissel primary en shadow. Nu bedient Solid Cache reads en is Redis de fallback. Bij een cache miss in Solid Cache leest de class Redis en vult Solid Cache aan:
def read(key, options = nil)
value = @primary.read(key, options)
return value unless value.nil?
redis_value = @shadow.read(key, options)
@primary.write(key, redis_value, options) if redis_value
redis_value
end
Kijk goed naar je cache hit rate-metric. Die zakt in het eerste uur terwijl Solid Cache zich vult uit Redis-reads. Als de steady-state hit rate meer dan 3 procentpunten onder je Redis-baseline ligt, is je max_size te klein — verhoog hem voor je verder gaat.
Fase 4: Redis verwijderen
Na een week op Solid Cache-reads met Redis als fallback en geen incidenten, vervang je de DualWriteCache door de gewone Solid Cache store en beëindig je het Redis-cluster:
config.cache_store = :solid_cache_store
Verwijder de ElastiCache-instance. Zeg de rekening op. Update config/database.yml om de Redis URL te verwijderen. Dit is het moment waarop ik meestal een screenshot maak voor het cost-dashboard van de klant.
Cache Hit Rate Monitoren in Productie
De enige productiemetric die je vertelt of je cache gezond is, is hit rate. Onder de 80% en je cache is waarschijnlijk te klein of je TTL’s zijn te agressief. Boven de 99% en je cachet mogelijk dingen die nooit veranderen en statisch zouden moeten zijn.
ActiveSupport::Notifications publiceert cache_read.active_support en cache_write.active_support events voor elke cache-operatie. Subscribe één keer in een initializer en push counters naar je metrics-backend:
# config/initializers/cache_metrics.rb
ActiveSupport::Notifications.subscribe("cache_read.active_support") do |event|
hit = event.payload[:hit] ? "hit" : "miss"
StatsD.increment("rails.cache.read", tags: ["result:#{hit}"])
StatsD.timing("rails.cache.read_duration_ms", event.duration)
end
ActiveSupport::Notifications.subscribe("cache_write.active_support") do |event|
StatsD.increment("rails.cache.write")
StatsD.timing("rails.cache.write_duration_ms", event.duration)
end
Bereken hit rate als hits / (hits + misses) in je dashboard, gesegmenteerd per minuut. Alerteer wanneer die langer dan 15 minuten onder de 75% zakt — dat betekent meestal dat de cache te agressief wordt geëvicteerd of dat een deploy een verandering heeft geshipt die een hot key invalideerde.
Voor Solid Cache specifiek, tracken we ook de solid_cache_entries tabel row count en byte size:
# lib/tasks/solid_cache_metrics.rake
namespace :solid_cache do
task metrics: :environment do
stats = SolidCache::Entry.connection.execute(<<~SQL).first
SELECT count(*) as rows, sum(byte_size) as bytes
FROM solid_cache_entries
SQL
StatsD.gauge("solid_cache.entries.count", stats["rows"])
StatsD.gauge("solid_cache.entries.bytes", stats["bytes"] || 0)
end
end
Plan die elke minuut via Solid Queue recurring jobs. Als de byte count langere tijd binnen 10% van max_size blijft, werkt de trimmer correct. Als hij max_size overschrijdt en daar blijft, loopt de trimmer achter en moet je trim_batch_size verhogen.
Wanneer Solid Cache Niet te Gebruiken
Solid Cache vervangt Redis-als-cache. Het vervangt geen Redis-als-alles-anders. Houd Redis als je het gebruikt voor:
- Sidekiq-queues — hoewel, als je Sidekiq ook puur als job queue gebruikt, kijk dan naar migreren naar Solid Queue tegelijkertijd.
- Rate limiting met atomic counters — Solid Cache’s
incrementwerkt, maar bij honderden ops per seconde wordt het een database hot row. - Pub/Sub voor Action Cable — Solid Cable bestaat ook in Rails 8, maar dat is een aparte migratie.
- Cross-application shared cache — als twee Rails-apps een cache delen, houd ze dan op Redis. Solid Cache is scoped tot één database.
Ik raad Solid Cache ook af op cache-workloads boven ruwweg 5.000 writes per seconde op een gedeelde database. Onder die write rate is de impact op je primary database verwaarloosbaar; erboven zie je wait time op WAL flushes voor de cache-database die doorwerkt in je applicatiequeries. Verhuis de cache in dat geval naar een dedicated Postgres-instance — waar de database: cache config voor is ontworpen.
Het andere geval waarvoor ik Redis houd is intensief gebruik van Rails.cache.write(key, value, race_condition_ttl:). Solid Cache ondersteunt het, maar het write-plus-read-plus-refresh patroon vereist een database-transactie en meerdere round trips. Op Redis is dezelfde operatie één commando. Als de meeste van je cache-writes deze optie gebruiken, benchmark voor je migreert.
De Kosten-en-Complexiteit Rekensom
Voor de meeste Rails-applicaties waar ik mee werk komen de getallen ergens hierop uit. Redis op ElastiCache met een cache.t3.small instance kost $18–25 per maand plus data transfer. Een productie-sized ElastiCache-cluster met een replica is $150–400 per maand. De cache-workload op Postgres voegt een meetbare maar kleine hoeveelheid toe aan je primary database-load — bij de klant die ik noemde waren cache-reads na migratie 8% van hun totale Postgres query count, maar slechts 0,4% van de query-tijd omdat elk een single-row primary-key lookup was.
De operationele simplificatie is groter dan de kostenbesparing. Eén service minder om te monitoren, één set security patches minder, één ding minder dat kan falen tijdens een deploy, één ding minder om over na te denken bij het debuggen van productie. In teams onder de vijf engineers is die reductie in oppervlak meer waard dan de bespaarde dollars.
Rails 8 Solid Cache is niet geschikt voor elke applicatie. Maar voor de standaard Rails-app die Sidekiq-in-Postgres en Redis-als-cache-only draait, is Redis verwijderen een tweeweeks project dat je infrastructuur-footprint permanent verkleint. De tooling is nu goed genoeg om dat een saaie beslissing te maken in plaats van een risicovolle.
FAQ
Is Rails 8 Solid Cache sneller dan Redis?
Nee. Een Redis GET retourneert typisch in 0,2–0,5 ms; een Solid Cache read raakt Postgres en retourneert in 0,5–2 ms. Voor de fragment caching, view caching en gememoizede query results die 95% van Rails cache-workloads uitmaken, is het verschil onzichtbaar in end-to-end response times. Als je applicatie afhankelijk is van sub-milliseconde cache latency voor een specifieke operatie, houd Redis voor die operatie en gebruik Solid Cache voor de rest.
Vereist Solid Cache een aparte database?
Nee, maar ik raad het sterk aan in productie. Je kunt Solid Cache naar je primary applicatiedatabase wijzen en het zal daar de solid_cache_entries tabel aanmaken. Op low-traffic apps is dat prima. Op elke app die meer dan een paar honderd cache-writes per seconde doet, zet de cache-tabel in zijn eigen database — de WAL-activiteit van cache-writes concurreert met je applicatietransacties om I/O. De database: cache config in config/cache.yml regelt dit netjes.
Hoe leeg ik de Solid Cache in productie zonder downtime?
Gebruik Rails.cache.clear vanuit een Rails console — het truncate de solid_cache_entries tabel. Dit is veiliger dan de equivalente Redis FLUSHALL omdat de operatie transactioneel is en andere database-operaties niet blokkeert. Voor een partiële invalidatie, gebruik namespace scoping in Rails.cache.delete_matched (ondersteund in Solid Cache) of verhoog een cache key version constant om misses op de betrokken keys te forceren.
Kan ik Solid Cache gebruiken met een read replica om reads te schalen?
Niet direct. Solid Cache doet zijn eigen routing via Rails multi-database configuratie, maar read replicas van de cache-database verslaan het doel — een cache miss op een replica gevolgd door een write naar de primary nodigt uit tot replication-lag bugs. Als je cache-reads moet schalen, ofwel shard je de cache-database over meerdere writers (Solid Cache’s shards: optie regelt dit), of accepteer je dat één Postgres-instance tienduizenden cache-reads per seconde kan bedienen wanneer de working set in RAM past.
Redis uit een Rails 8-app slopen? TTB Software helpt teams cache-infrastructuur te migreren, hun databases juist te dimensioneren en hun productiestack te simplificeren. Negentien jaar Rails, en ik help je met plezier een service te verwijderen in plaats van er een toe te voegen.
Related Articles
Rails Thruster: Vervang Nginx door de Ingebouwde HTTP/2-proxy van Rails 8 in Productie
Rails Thruster vervangt Nginx als HTTP/2-proxy in Rails 8 productie. Configuratiegids: TLS via ACME, compressie, Kama...
Rails LLM Observability: Prompts, Latentie en Tokengebruik Tracen in Productie met Langfuse
Rails LLM observability-gids: trace prompts, latentie en tokenkosten in productie met Langfuse en ActiveSupport::Noti...
Rails Transactional Outbox Pattern: Betrouwbaar Events Publiceren Zonder Dual-Write Fouten
Rails transactional outbox pattern: voorkom dual-write fouten en verloren webhooks door events atomair te publiceren ...