igmarin/elixir-phoenix-skills

cachex-caching

MANDATORY for implementing caching in Elixir applications. Invoke before adding caching layers. Configures Cachex instances, implements cache-aside and get-or-set patterns, sets TTL policies, builds cache warmers, monitors cache statistics, and sets up distributed caching across nodes. Trigger words: Cachex, caching, cache, TTL, ETS, distributed cache, cache warmer, cache warmup, cache invalidation, cache hits, cache misses, Cachex.fetch, Cachex.put, Cachex.get.

First seen Jun 20, 2026

Installation

$ npx skills add igmarin/elixir-phoenix-skills --skill cachex-caching

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from igmarin/elixir-phoenix-skills · top by installs.

npx skills add igmarin/elixir-phoenix-skills

Browse all from igmarin/elixir-phoenix-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT
More metadata
version
1.0.0
user-invocable
true

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,448 B
  • docs SUMMARY.md 488 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 14 installs

SKILL.md

Cachex Caching

Canonical FP bar: [docs/fcis-engineering-rules.md](../../docs/fcis-engineering-rules.md) — Functional Core, Imperative Shell: pure domain modules; side effects at edges. Workers and pipelines are edges: fetch IDs, call pure core, return tagged tuples.

RULES — Follow these with no exceptions

1. Set a TTL on every cached entry — never cache indefinitely unless the data is truly immutable 2. Enable stats: true — required to measure cache effectiveness via hit_rate 3. Use cache warmers for startup — pre-populate expensive data when the application starts 4. Use Cachex.fetch/3 for get-or-set — atomic cache-aside avoids the dogpile/race on concurrent misses 5. Invalidate with Cachex.del/2 after every mutation — stale entries outlive the source of truth otherwise 6. Handle the {:error, reason} tuple from every Cachex call — fall back to the database; a cache failure must never break the request 7. Bound the cache with :limit and an eviction policy — unbounded caches leak memory

End-to-End Workflow

Follow this sequence when adding caching to a feature:

  1. Add dependency — add {:cachex, "~> 3.6"} to mix.exs and run mix deps.get
  2. Configure cache — start a named Cachex instance in your application supervisor
  3. Implement get-or-set — use Cachex.fetch/3 for atomic cache-aside pattern
  4. Add invalidation — call Cachex.del/2 after mutating data
  5. Enable stats — configure stats: true in cache options
  6. Add cache warmer — for expensive data, pre-populate on startup
  7. Verify hit rate — call Cachex.stats/1 and confirm hit_rate is non-zero; see Monitoring Cache Stats for interpretation thresholds and remediation guidance
  8. Monitor in production — emit telemetry events for cache operations

Setup

# mix.exs
defp deps do
  [
    {:cachex, "~> 3.6"}
  ]
end

In your application supervisor:

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Basic cache with 1000 entry limit
      {Cachex, name: :my_cache, limit: 1000},
      
      # Cache with stats enabled for monitoring
      {Cachex, name: :stats_cache, limit: 5000, stats: true},
      
      # Cache with TTL, LRW eviction policy, stats, and hooks
      {Cachex,
       name: :ttl_cache,
       limit: 10_000,
       ttl: :timer.minutes(10),
       policy: Cachex.Policy.LRW,
       stats: true,
       hooks: [
         %Cachex.Hook{module: MyApp.CacheLogger}
       ]}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Basic Operations

case Cachex.get(:my_cache, "user:123") do
  {:ok, nil} -> :miss
  {:ok, value} -> {:hit, value}
end

# put with explicit TTL (overrides cache-level default)
Cachex.put(:my_cache, "session:abc", data, ttl: :timer.minutes(30))

Cachex.del(:my_cache, "user:123")
Cachex.clear(:my_cache)

Get-or-Set Pattern

# Cachex.fetch/3 — atomic cache-aside, avoids race conditions
{status, value} =
  Cachex.fetch(:my_cache, "user:123", fn key ->
    user = MyApp.Accounts.get_user(123)
    {:commit, user, ttl: :timer.minutes(5)}
  end)

case status do
  :ok -> IO.puts("Cache hit")
  :commit -> IO.puts("Cache miss - computed and cached")
end

Cache Warmers

defmodule MyApp.CacheWarmer do
  use Cachex.Warmer

  def execute(state) do
    users = MyApp.Accounts.list_active_users()

    actions =
      Enum.map(users, fn user ->
        {:put, "user:#{user.id}", user, ttl: :timer.hours(1)}
      end)

    {:ok, actions}
  end
end

children = [
  {Cachex,
   name: :my_cache,
   warmers: [
     %Cachex.Warmer{module: MyApp.CacheWarmer, interval: :timer.minutes(5)}
   ]}
]

Cache Invalidation

# All Cachex operations return {:ok, result} or {:error, reason}; fall back to DB on failure
defmodule MyApp.Accounts do
  def update_user(user, attrs) do
    with {:ok, updated_user} <- Repo.update(User.changeset(user, attrs)) do
      case Cachex.del(:my_cache, "user:#{user.id}") do
        {:ok, _} ->
          :ok

        {:error, reason} ->
          Logger.warning("Cache invalidation failed for user:#{user.id}: #{inspect(reason)}")
      end

      {:ok, updated_user}
    end
  end

  def get_user(id) do
    case Cachex.fetch(:my_cache, "user:#{id}", fn _ ->
      {:commit, Repo.get(User, id), ttl: :timer.minutes(5)}
    end) do
      {:ok, user} -> user
      {:error, _} -> Repo.get(User, id)  # Fallback to database
    end
  end
end

Monitoring Cache Stats

{Cachex, name: :my_cache, stats: true}

{:ok, stats} = Cachex.stats(:my_cache)
# %{hit_rate: 85.5, hits: 8550, misses: 1450, gets: 10000, sets: 1200, evictions: 100}

Interpret hit rate:

  • > 80% — excellent, cache is very effective
  • 60-80% — good, normal for read-heavy workloads
  • < 60% — investigate TTL values and check for over-invalidation
  • < 20% — cache may be ineffective; revisit TTL strategy and key design before deploying to production

Telemetry Integration

Emit telemetry events for cache operations to monitor in production:

def get_user(id) do
  case Cachex.fetch(:my_cache, "user:#{id}", fn _ ->
    {:commit, Repo.get(User, id)}
  end) do
    {:ok, user} ->
      :telemetry.execute([:my_app, :cache, :my_cache], %{hits: 1}, %{key: "user:#{id}"})
      user
    {:error, _} ->
      :telemetry.execute([:my_app, :cache, :my_cache], %{misses: 1}, %{key: "user:#{id}"})
      Repo.get(User, id)
  end
end

Distributed Caching

Broadcast-based invalidation across nodes:

defmodule MyApp.CacheSync do
  @topic "cache:invalidate"

  def broadcast_delete(key) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, @topic, {:invalidate, key})
  end

  def handle_info({:invalidate, key}, state) do
    Cachex.del(:my_cache, key)
    {:noreply, state}
  end
end

# Subscribe in your GenServer or LiveView
Phoenix.PubSub.subscribe(MyApp.PubSub, "cache:invalidate")

Remote reads via RPC (single authoritative node pattern):

def get_user_distributed(id) do
  case Cachex.get(:my_cache, "user:#{id}") do
    {:ok, nil} ->
      :rpc.call(primary_node(), Cachex, :get, [:my_cache, "user:#{id}"])
      |> case do
        {:ok, nil} -> fetch_from_db(id)
        {:ok, value} -> value
      end

    {:ok, value} ->
      value
  end
end

defp primary_node, do: Application.fetch_env!(:my_app, :primary_cache_node)

Common Pitfalls

❌ Don't ✅ Do
Cache with no TTL Set a per-entry TTL (or a cache-level default) so stale data expires
Do Cachex.get/2 + Cachex.put/3 for cache-aside Use Cachex.fetch/3 — atomic get-or-set avoids concurrent-miss races
Ignore Cachex return tuples Match {:ok, } / {:error, } and fall back to the database on error
Forget to invalidate after writes Call Cachex.del/2 after every mutation of the cached record
Run a cache without stats Enable stats: true and watch hit_rate to confirm effectiveness
Let the cache grow unbounded Set :limit with an eviction policy (e.g. Cachex.Policy.LRW)
Assume a local cache is shared across nodes Broadcast invalidation via PubSub or read from an authoritative node via RPC

Integration

Predecessor This Skill Successor
ecto-essentials cachex-caching telemetry-essentials
otp-essentials cachex-caching deployment-gotchas

Companion skills:

  • telemetry-essentials — emit and monitor cache hit/miss events in production
  • phoenix-pubsub-patterns — broadcast cross-node cache invalidation