Caching

SkillDatabases & data

Decide what to cache, at which layer, for how long, and how it gets invalidated, before any cache is added. Use when someone proposes caching to make a read path faster, when stale or cross-user data is suspected, when adding a TTL or a Redis/CDN layer, or when reviewing a change that introduces one. Not for fixing a slow query or an N+1 (do that first, a cache over a bad query hides it), and not for HTTP header tuning with no server-side store.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Caching skill

What this skill tells your AI

The instructions your AI receives, as published by nahid-sparktales/agent-dispatcher in skills/backend/caching/SKILL.md and read by ahel’s review.

A cache is a second copy of the truth that is allowed to be wrong. Adding one trades a latency problem for a correctness problem, and the correctness problem arrives later, in production, as "why is this user seeing yesterday's number".

Never add a cache before you can say, in one sentence each: what is cached, keyed by what, for how long, and what makes it wrong.

When this fires

A read path is slow and caching is being proposed; a cache already exists and data is stale or leaking across users; a review touches a TTL, a memoize decorator, a Redis client, or a CDN rule. It does not fire for a write path, or for a slow query nobody has profiled yet.

Procedure

  1. Measure the uncached path first. Get a real number — p50 and p95, and how often it is called. Without it you cannot tell afterwards whether the cache helped, and you have no basis for the TTL. A cache added on a hunch is a guess with extra failure modes.
  2. Try to make the cache unnecessary. A missing index, an N+1, a payload fetching columns nobody reads, or a call made in a loop are all cheaper to fix than to cache, and the fix has no staleness. Only cache what is genuinely expensive and genuinely re-read.
  3. Classify the data by staleness tolerance, and let that pick the TTL — not the other way round. Ask the person who owns the data, in seconds: how long may this be wrong? Pricing, permissions, balances and anything a user just edited usually answer "not at all", which means either no cache or write-through invalidation, never a hopeful TTL.
  4. Pick the layer deliberately. Each one fails differently — see below. Caching the same value at three layers means three different answers and three invalidation paths.
  5. Design the key before the read. The key must contain every input that changes the value: identity or tenant, authorization scope, locale, feature flags, API version, and the query parameters that matter. A key missing the tenant is a cross-tenant data leak, and it will look exactly like a cache working well. Never key on a mutable object's memory identity.
  6. Write the invalidation path in the same change as the read path. Decide which of these you are using and say so: TTL expiry only; explicit delete or overwrite on every write; or versioned keys (put a version or updated-at in the key so a change makes old entries unreachable and eviction cleans them up). Versioned keys are the least fragile — nothing to purge, no fan-out — and are the default worth reaching for.
  7. Decide what happens when the cache is unavailable. A cache whose outage takes the app down is not a cache, it is an undeclared database. The read path should fall through to the source on error and timeout, with a short timeout, and the fallback must be load the source can actually survive.
  8. Handle the stampede. When a hot key expires under load, every concurrent request misses at once and hits the source together. Mitigate with a single-flight lock per key, a jittered TTL so keys do not expire in lockstep, or serving stale while one worker refreshes.
  9. Decide about negative results. Caching "not found" stops a hammering miss, but it also caches a race: a row created right after the negative entry stays invisible for the TTL. Cache negatives for seconds, not minutes, and invalidate them on create.
  10. Make it observable. Emit hit rate, miss rate, and error/fallback count per cache. A hit rate you cannot see is a cache you cannot reason about, and a hit rate near zero means you have added a network hop and nothing else.
  11. Re-measure, then prove invalidation. Write a value, read it back through the cached path, and confirm the new value appears. This is the step that is skipped and the one that catches real bugs — a fast wrong answer is worse than the slow right one.

The layers and how each one fails

  • Browser / HTTP response cache. Cannot be invalidated, only expired — anything cached here is out of your hands until the TTL passes. Never cache a personalized or authorized response in a shared cache; get the public/private distinction wrong and one user's page is served to another. Version the URL when you need a hard cut.
  • CDN / edge. Same exposure, plus: purges are eventually consistent across locations, and keying is by URL unless you declare which request headers vary the response. A missing vary declaration is the classic cross-user leak. Purging production edge cache is an outward-facing action — say what will be purged and ask before doing it.
  • In-process (LRU, memoize, module-level dict). Cheapest and most local. But each instance holds its own truth, so N replicas give N answers and invalidation does not fan out; entries live in the heap and an unbounded one is a memory leak; it empties on deploy, which is why bugs here vanish when you restart and come back an hour later. Fine for immutable or derived-from-input data, dangerous for anything a user can edit.
  • Shared cache (Redis, memcached). One truth for all instances and invalidation that fans out, at the cost of a network hop and a new dependency you must be able to lose. Under memory pressure it evicts — including keys you assumed were there — so nothing may be stored only in the cache. Flushing a shared production cache is destructive: it can stampede the database. Stop and ask before any flush; prefer deleting the specific keys.
  • Database-side (materialized view, summary table). Consistent with the source at refresh time and queryable, but stale between refreshes and the refresh itself costs and may lock. Know what a refresh does to concurrent readers before scheduling one.
  • Write-through denormalized column. Fastest read of all; the correctness burden moves onto every writer. One write path that forgets to update it — a migration, a backfill, an admin script, a second service — leaves the value permanently wrong with no TTL to heal it. Only take this on with a reconciliation job that can detect and repair drift.

Checklist

  • Uncached cost measured, not assumed
  • Cheaper non-cache fix considered and ruled out in one line
  • Staleness tolerance stated in seconds by someone who owns the data
  • Layer chosen, with its failure mode named
  • Key contains tenant/identity, authorization scope, and every varying input
  • Invalidation strategy written in the same change as the read
  • Behaviour on cache outage and on stampede is defined
  • Hit rate and fallback count are emitted
  • Write-then-read-back actually executed and observed

Failure handling

  • Stale data reported in production — do not raise the TTL question first. Find which layer is serving it: check the source, then each cache in front of it, and find where the answers diverge. The layer that diverges is the one with the broken invalidation.
  • One user seeing another user's data — treat as a security incident, not a cache bug. Stop, report it, and check the key and any vary declaration before anything else.
  • Hit rate near zero — the key varies more than the data does. Usually a timestamp, a request id, or a serialized object with unstable ordering is in the key. Remove the cache until the key is fixed; it is currently pure overhead.
  • It got faster but you cannot say by how much — you skipped step 1. Say so rather than reporting an improvement you did not measure.
  • Cannot reproduce staleness locally — expected, and not evidence it is fixed. Single-instance local runs hide per-instance cache divergence entirely. Say it is unreproduced locally.

Evidence to report

Before and after numbers from the same measurement, with the method named. The cache key format written out literally. The TTL and the staleness tolerance it came from. The invalidation path, and the result of the write-then-read-back check. Which layers now hold a copy of this value. What is still unverified — multi-instance behaviour, cache-down fallback, and stampede under load are the three most commonly claimed and least commonly tested.

Signals

GitHub stars
20
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
caching-nahid-sparktales
Source
github.com/nahid-sparktales/agent-dispatcher