Hit Rate

performance

Definition and Overview

Hit rate is the percentage of cache lookups that find the requested data in the cache (cache hits) versus the total number of lookups attempted. It is calculated as: hit_rate = cache_hits / (cache_hits + cache_misses) * 100%. A 95% hit rate means that 95 out of every 100 lookups are served from the cache without needing to access the slower backend data source (database, API, file system).

Hit rate is the single most important metric for evaluating cache effectiveness. A cache with a high hit rate (>90%) dramatically reduces load on backend systems and improves response times, since cache lookups (especially from ETS) are orders of magnitude faster than database queries or API calls. A cache with a low hit rate (<50%) provides little benefit while consuming memory and adding code complexity. Understanding and optimizing hit rate is essential for meeting the Prismatic Platform's performance requirements.

The relationship between hit rate and performance is non-linear. Improving hit rate from 0% to 50% halves backend load. Improving from 90% to 95% halves the remaining backend load again (from 10% to 5%). Improving from 95% to 99% reduces remaining load by 80%. Each percentage point of improvement at the high end has disproportionately large impact. This is why high-performance systems invest significant effort in achieving hit rates above 95%.

Technical Deep Dive

Hit Rate Impact Analysis

Hit RateMiss RateBackend Load FactorEffective Latency Reduction
0%100%1.0x (no cache)None
50%50%0.5xModerate
80%20%0.2xSignificant
90%10%0.1xLarge
95%5%0.05xVery large
99%1%0.01xNear-complete offload
99.9%0.1%0.001xEssentially zero backend

Factors Affecting Hit Rate

FactorImpactOptimization
Cache sizeLarger cache holds more data, fewer evictionsSize to fit working set
TTL (Time to Live)Shorter TTL = more misses, fresher dataBalance freshness vs hit rate
Eviction policyLRU, LFU, FIFO affect which entries are keptMatch policy to access pattern
Working set sizeSmaller working set = higher hit rateIdentify hot data subset
Access patternZipf (few hot keys) vs uniform affects hit rateZipf patterns cache well
Cache warmingPre-loading cache reduces cold-start missesWarm on boot or deployment
Key distributionHot spots vs even distributionShard if necessary

Eviction Policies

PolicyDescriptionBest For
LRU (Least Recently Used)Evict least recently accessedGeneral purpose, temporal locality
LFU (Least Frequently Used)Evict least frequently accessedFrequency-based access patterns
FIFO (First In First Out)Evict oldest entriesSimple, predictable behavior
TTL (Time to Live)Evict after fixed timeData freshness requirements
RandomEvict random entryUniform access, simplest implementation
ARC (Adaptive Replacement)Self-tuning LRU/LFU hybridVariable access patterns

Architecture and Implementation

Cache hit rate monitoring architecture consists of three components: instrumentation (counting hits and misses at cache lookup points), aggregation (computing rates over time windows), and alerting (notifying when rates drop below thresholds).

The instrumentation layer wraps cache operations with counters that track hits and misses. In the BEAM ecosystem, this is typically implemented using :telemetry events emitted on each cache access, or atomic counters in ETS. The aggregation layer computes rates over sliding time windows (1 minute, 5 minutes, 1 hour) to smooth out temporary fluctuations. The alerting layer triggers notifications when the hit rate drops below configured thresholds, indicating potential issues such as cache eviction storms, working set changes, or cache invalidation bugs.

Usage in Prismatic Platform

The Prismatic Platform tracks hit rates for ETS-based caches across all registries and lookup services.

defmodule Prismatic.Cache.HitRateTracker do
  @moduledoc """
  Tracks and reports cache hit rates across platform
  caches. Provides real-time monitoring and alerting
  when hit rates drop below acceptable thresholds.
  """

  use GenServer

  @type cache_stats :: %{
    cache_name: atom(),
    hits: non_neg_integer(),
    misses: non_neg_integer(),
    hit_rate: float(),
    window_seconds: pos_integer()
  }

  @alert_threshold 0.80
  @check_interval_ms 60_000

  @spec record_hit(atom()) :: :ok
  def record_hit(cache_name) do
    :telemetry.execute(
      [:prismatic, :cache, :hit],
      %{count: 1},
      %{cache: cache_name}
    )
  end

  @spec record_miss(atom()) :: :ok
  def record_miss(cache_name) do
    :telemetry.execute(
      [:prismatic, :cache, :miss],
      %{count: 1},
      %{cache: cache_name}
    )
  end

  @spec get_stats(atom()) :: cache_stats()
  def get_stats(cache_name) do
    GenServer.call(__MODULE__, {:get_stats, cache_name})
  end

  @spec get_all_stats() :: list(cache_stats())
  def get_all_stats do
    GenServer.call(__MODULE__, :get_all_stats)
  end

  @impl GenServer
  def init(_opts) do
    :telemetry.attach_many(
      "cache-hit-rate-tracker",
      [[:prismatic, :cache, :hit], [:prismatic, :cache, :miss]],
      &handle_telemetry_event/4,
      %{}
    )

    schedule_check()
    {:ok, %{counters: %{}}}
  end

  @impl GenServer
  def handle_call({:get_stats, cache_name}, _from, state) do
    stats = compute_stats(cache_name, state.counters)
    {:reply, stats, state}
  end

  @impl GenServer
  def handle_call(:get_all_stats, _from, state) do
    all_stats =
      state.counters
      |> Map.keys()
      |> Enum.map(fn cache -> compute_stats(cache, state.counters) end)
      |> Enum.sort_by(& &1.hit_rate)

    {:reply, all_stats, state}
  end

  @impl GenServer
  def handle_info(:check_rates, state) do
    Enum.each(state.counters, fn {cache, _} ->
      stats = compute_stats(cache, state.counters)
      if stats.hit_rate < @alert_threshold and stats.hits + stats.misses > 100 do
        :telemetry.execute(
          [:prismatic, :cache, :low_hit_rate],
          %{hit_rate: stats.hit_rate},
          %{cache: cache, threshold: @alert_threshold}
        )
      end
    end)

    schedule_check()
    {:noreply, state}
  end

  defp compute_stats(cache_name, counters) do
    {hits, misses} = Map.get(counters, cache_name, {0, 0})
    total = hits + misses
    rate = if total > 0, do: hits / total, else: 0.0

    %{cache_name: cache_name, hits: hits, misses: misses, hit_rate: rate, window_seconds: 60}
  end

  defp handle_telemetry_event([:prismatic, :cache, :hit], _measurements, metadata, _config) do
    GenServer.cast(__MODULE__, {:hit, metadata.cache})
  end

  defp handle_telemetry_event([:prismatic, :cache, :miss], _measurements, metadata, _config) do
    GenServer.cast(__MODULE__, {:miss, metadata.cache})
  end

  defp schedule_check, do: Process.send_after(self(), :check_rates, @check_interval_ms)
end

The platform monitors hit rates for the OSINT ToolRegistry ETS cache (target >99%), the Academy TopicRegistry (target >99%), the DD SourceRegistry (target >99%), and the Meilisearch query cache (target >90%). When hit rates drop below thresholds, telemetry alerts are emitted and displayed on the platform administration dashboard.

Cross-References

  • ETS Memory -- ETS tables as primary cache storage
  • Execution Time -- Cache impact on operation timing
  • Performance -- Broader performance optimization
  • Telemetry -- Metric collection for hit rate tracking
  • Livebooks: performance_monitoring/ notebooks include cache analysis tools
  • Academy: Performance topics cover caching strategies and hit rate optimization

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL