agentleFS
jars-demojars-demo/jars-skills/skills/caching/SKILL.md

caching

Skill0 starsChanged 32 days agoMITView on GitHub

Add caching to reduce latency and database load. Use when the user asks to add a cache, speed up repeated queries, set up Redis, implement cache invalidation, add HTTP caching headers, use a CDN, fix stale cache bugs, or decide what to cache. Never caches blindly — measures first and picks the right strategy for the data.

  • Reads credentials

14 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use convention-learner, opentelemetry and 4 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • codewithmukeshSkill

    convention-learner

    Detects and enforces project-specific coding conventions by analyzing existing codebase patterns. Learns naming conventions, folder structure, test organization, and coding style from the existing code. Load when: "conventions", "coding standards", "project patterns", "enforce style", "detect patterns", "learn conventions", "code consistency".

    Also in 3 projects that use this

  • codewithmukeshSkill

    opentelemetry

    OpenTelemetry observability for .NET 10 applications. Covers traces, metrics, and logs using the OpenTelemetry SDK with OTLP export. Includes custom ActivitySource, IMeterFactory metrics, resource configuration, and Aspire Dashboard integration. Load this skill when setting up distributed tracing, custom metrics, OTLP export, or when the user mentions "OpenTelemetry", "OTLP", "traces", "spans", "Activity", "ActivitySource", "metrics", "IMeterFactory", "Meter", "Counter", "Histogram", "Gauge", "telemetry", "observability", "distributed tracing", "OTEL", or "Aspire Dashboard".

    Also in 3 projects that use this

  • MovGP0Skill

    modern-csharp

    Modern C# style guidance for Bendex and MicroSea code changes, including primary constructors, expression-bodied members, var usage, target-typed new, lambdas, and pattern matching.

    Also in 3 projects that use this

  • MicrosoftSkill

    resilience

    Apply resilience patterns (retry, backoff, circuit breaking) when writing or reviewing code that makes outbound HTTP/REST calls, database operations, or any I/O that can experience transient failures. Use this skill when: (1) writing new code that calls external services or APIs, (2) reviewing code that uses fetch, HTTP clients, or service-to-service calls, (3) adding queue consumers or workers that interact with remote systems, (4) debugging intermittent failures in production.

    Also in 4 projects that use this

  • MicrosoftSkill

    authentication

    Wire this in whenever the app uses or connects to data — records, a database, entities, per-user data, row-level security — or when the user wants sign-in, accounts, login, or protected pages. Rayfin data is always accessed as an authenticated user, so **data implies auth**; only a static page over public data skips it. The starter ships as a no-auth "hello world" that previews with no backend, and the full Fabric auth scaffolding is already in the project — this skill wires it in: AuthProvider + bootstrapAuth in main.tsx, the route guard in App.tsx, the sign-in page, sign-out, and the env/deploy requirement. Triggers: auth, authentication, sign in, sign-in, login, log in, sign out, logout, account, user, identity, protect route, require login, gated page, data, database, records, per-user, row-level security, who is the current user, Fabric auth, Entra.

    Also in 5 projects that use this

  • hongyuancSkill

    vertical-slice

    Build and evaluate a production-quality vertical slice before advancing from Pre-Production to Production.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 3 are distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 11 edited versions we read keep these.

  • 3

    Distinctive

    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:

    Never share a key between different data shapes.

    line 136
  2. Distinctive:

    When in doubt, start with a shorter TTL.

    line 155
  3. Distinctive:

    Delete the exact key when the data changes.

    line 164

“Common” is measured: kept by at least 6 in 10 of the 11 edited versions of this file we could read. “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

Caching

Cache the right things, invalidate correctly, and avoid serving stale data. A wrong cache is worse than no cache — it serves incorrect results silently.

Ground rules

  • Measure before caching. Confirm the operation is actually slow or expensive. Don't cache speculatively.
  • Understand the data before choosing a TTL. How often does it change? How bad is it if a user sees stale data for 30 seconds? 5 minutes? 1 hour?
  • Cache at the highest safe layer. HTTP caching > CDN > application cache > database query cache. Higher layers serve more requests for less cost.
  • Every cache needs an invalidation plan. A TTL is a fallback, not a strategy. Know exactly when the cached value is wrong and evict it then.
  • Never cache secrets, tokens, or per-user sensitive data in a shared cache without scoping the key to the user.

Step 0: Check what's already there

# Existing cache clients
grep -r "redis\|ioredis\|memcached\|lru-cache\|node-cache\|cachetools\|go-redis" \
  --include="*.ts" --include="*.js" --include="*.py" --include="*.go" -l | head -10

# Existing cache usage patterns
grep -rn "\.get\|\.set\|\.del\|cache\." --include="*.ts" --include="*.py" | head -20

# Docker / infra: is Redis already running?
cat docker-compose.yml | grep -A5 redis
cat .env | grep REDIS

Choosing a caching strategy

StrategyHow it worksUse when
Cache-asideApp checks cache first; on miss, loads from DB and writes to cacheMost common. Read-heavy data that changes occasionally.
Write-throughApp writes to cache and DB together on every writeData that must be in cache immediately after write.
Write-behindApp writes to cache; cache asynchronously persists to DBHigh write throughput where slight data loss on crash is acceptable. Rare.
Read-throughCache fetches from DB on miss automaticallyORM-level caches; less common in application code.
Refresh-aheadCache proactively refreshes before TTL expiresPredictable access patterns; avoids latency spikes on expiry.

For most web apps: cache-aside with explicit invalidation on mutation.


Cache-aside pattern

// TypeScript — Redis with ioredis
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

async function getProduct(id: string) {
  const key = `product:${id}`;

  // 1. Check cache
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  // 2. Miss — load from DB
  const product = await db.product.findUnique({ where: { id } });
  if (!product) return null;

  // 3. Populate cache with TTL
  await redis.set(key, JSON.stringify(product), "EX", 300);  // 5 minutes

  return product;
}

// Invalidate on mutation
async function updateProduct(id: string, data: Partial<Product>) {
  const updated = await db.product.update({ where: { id }, data });
  await redis.del(`product:${id}`);  // evict immediately
  return updated;
}
# Python — Redis with redis-py
import redis, json
from functools import wraps

r = redis.from_url(os.environ["REDIS_URL"], decode_responses=True)

def get_product(product_id: str):
    key = f"product:{product_id}"

    cached = r.get(key)
    if cached:
        return json.loads(cached)

    product = db.query(Product).filter_by(id=product_id).first()
    if not product:
        return None

    r.setex(key, 300, json.dumps(product.to_dict()))
    return product

def update_product(product_id: str, data: dict):
    product = db.query(Product).filter_by(id=product_id).first()
    for k, v in data.items():
        setattr(product, k, v)
    db.commit()
    r.delete(f"product:{product_id}")
    return product

Cache key design

Keys must be deterministic, unique, and scoped correctly.

# Pattern: <namespace>:<identifier>[:<variant>]
product:abc123
user:42:profile
search:q=shoes&page=2&sort=price
rate_limit:user:42:/api/checkout

Rules:

  • Include all inputs that affect the output in the key. If the result differs by locale, include locale: product:abc123:en.
  • Never share a key between different data shapes. Changing the serialization format will serve corrupt data to cache hits until TTL expires.
  • For per-user data in a shared Redis instance, always scope to user ID: dashboard:user:42 not dashboard.

TTL selection

Data typeSuggested TTL
Static content (images, fonts)1 year (versioned URL)
Product catalogue, blog posts5–15 minutes
User profile, settings1–5 minutes
Session dataDuration of session
Rate limit countersWindow size (e.g. 60 seconds)
Realtime data (stock price, inventory)5–30 seconds, or event-driven invalidation
Auth tokensToken expiry minus a small buffer

When in doubt, start with a shorter TTL. A cache miss costs a DB query; stale data costs user trust.


Cache invalidation patterns

Key-based eviction (simplest)

Delete the exact key when the data changes. Works when the cached item maps 1:1 to a DB row.

await redis.del(`product:${id}`);

Tag-based invalidation

Group related keys under a tag so you can evict them all at once. Useful when one DB record affects many cached views.

// On write: store which keys belong to a tag
await redis.sadd(`tag:product:${id}`, `product:${id}`, `homepage:featured`);

// On mutation: evict all tagged keys
async function invalidateByTag(tag: string) {
  const keys = await redis.smembers(`tag:${tag}`);
  if (keys.length) await redis.del(...keys, `tag:${tag}`);
}
await invalidateByTag(`product:${id}`);

Versioned keys (cache-busting)

Increment a version counter instead of deleting keys. Old keys expire naturally via TTL.

// Bump version on mutation
await redis.incr(`product:${id}:version`);

// Read: include version in the key
const version = await redis.get(`product:${id}:version`) ?? "1";
const key = `product:${id}:v${version}`;

HTTP caching headers

Set headers so browsers and CDNs cache responses — free performance with no Redis needed.

// Express — static public content (versioned assets)
res.set("Cache-Control", "public, max-age=31536000, immutable");

// Frequently changing public content
res.set("Cache-Control", "public, max-age=300, stale-while-revalidate=60");

// Authenticated or user-specific — never cache in shared cache
res.set("Cache-Control", "private, no-store");

// ETag-based conditional caching
import crypto from "crypto";
const etag = crypto.createHash("md5").update(JSON.stringify(data)).digest("hex");
res.set("ETag", `"${etag}"`);
if (req.headers["if-none-match"] === `"${etag}"`) {
  return res.status(304).send();
}
res.json(data);
Cache-Control directive cheat sheet:
  public              — CDNs and browsers may cache
  private             — browser only, no CDN
  no-store            — never cache (sensitive data)
  max-age=N           — cache for N seconds
  s-maxage=N          — CDN TTL (overrides max-age for CDNs)
  stale-while-revalidate=N  — serve stale while fetching fresh in background
  immutable           — content will never change (use with versioned URLs only)

Memoization (in-process cache)

For expensive pure computations within a single request or process lifetime:

// Simple LRU cache (no Redis needed)
import { LRUCache } from "lru-cache";

const cache = new LRUCache<string, unknown>({ max: 500, ttl: 1000 * 60 * 5 });

function memoize<T>(key: string, fn: () => Promise<T>, ttlMs?: number): Promise<T> {
  if (cache.has(key)) return Promise.resolve(cache.get(key) as T);
  return fn().then((result) => {
    cache.set(key, result);
    return result;
  });
}
# Python — functools.lru_cache for sync functions
from functools import lru_cache

@lru_cache(maxsize=256)
def get_exchange_rate(currency: str) -> float:
    return fetch_from_api(currency)

# For async functions — cachetools
from cachetools import TTLCache
_cache: TTLCache = TTLCache(maxsize=256, ttl=300)

async def get_exchange_rate(currency: str) -> float:
    if currency in _cache:
        return _cache[currency]
    rate = await fetch_from_api(currency)
    _cache[currency] = rate
    return rate

In-process caches don't survive restarts and aren't shared across instances. Use Redis for anything that must be consistent across multiple app servers.


Avoiding common cache bugs

BugCauseFix
Cache stampedeMany requests miss at the same time and hammer the DBUse a lock or probabilistic early expiry
Stale data after writeWrite path doesn't invalidate the cache keyEvict or update the key on every mutation
Caching null resultsA missing record is fetched from DB on every requestCache a sentinel value ("__NULL__") with a short TTL
Key collisionTwo features share a key prefix accidentallyUse namespaced keys (<service>:<type>:<id>)
PII in shared cacheUser A sees User B's dataInclude user ID in every user-scoped key
Unbounded in-process cacheMemory grows indefinitelyAlways set maxSize on LRU caches
Serialization mismatchCached JSON doesn't deserialize to the expected typeValidate or version the cached shape

Cache stampede prevention

// Use a short-lived lock so only one request fetches from DB on a cold miss
async function getWithLock<T>(key: string, fetch: () => Promise<T>, ttl: number): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${key}`;
  const locked = await redis.set(lockKey, "1", "NX", "EX", 5);  // 5s lock

  if (!locked) {
    // Another request is fetching — wait briefly and retry
    await new Promise((r) => setTimeout(r, 50));
    return getWithLock(key, fetch, ttl);
  }

  try {
    const result = await fetch();
    await redis.set(key, JSON.stringify(result), "EX", ttl);
    return result;
  } finally {
    await redis.del(lockKey);
  }
}

Checklist

  • Confirmed the target operation is actually slow before adding cache
  • Cache key includes all inputs that affect the output
  • Per-user data keys are scoped with the user ID
  • TTL chosen based on how stale the data can safely be
  • Every write path that mutates cached data also invalidates the key
  • Null/missing results are handled (cached sentinel or not cached)
  • In-process caches have a bounded maxSize
  • Shared Redis instance is not storing plaintext secrets or tokens
  • HTTP responses that vary by user have Cache-Control: private or no-store
  • Static assets with versioned URLs have Cache-Control: immutable

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.