agentleFS
igmarinigmarin/elixir-phoenix-skills/skills/cachex-caching/SKILL.md

cachex-caching

Skill4 starsChanged 4 months agoMITView on GitHub

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,

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Follow this sequence when adding caching to a feature:

    line 35

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Cachex Caching

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 TTLSet a per-entry TTL (or a cache-level default) so stale data expires
Do Cachex.get/2 + Cachex.put/3 for cache-asideUse Cachex.fetch/3 — atomic get-or-set avoids concurrent-miss races
Ignore Cachex return tuplesMatch {:ok, _} / {:error, _} and fall back to the database on error
Forget to invalidate after writesCall Cachex.del/2 after every mutation of the cached record
Run a cache without statsEnable stats: true and watch hit_rate to confirm effectiveness
Let the cache grow unboundedSet :limit with an eviction policy (e.g. Cachex.Policy.LRW)
Assume a local cache is shared across nodesBroadcast invalidation via PubSub or read from an authoritative node via RPC

Integration

PredecessorThis SkillSuccessor
ecto-essentialscachex-cachingtelemetry-essentials
otp-essentialscachex-cachingdeployment-gotchas

Companion skills:

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.