epic-caching
Implements caching for expensive queries and API responses using cachified and local storage based on performance optimization needs.
Install
mkdir -p .claude/skills/epic-caching && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/2290" && unzip -o skill.zip -d .claude/skills/epic-caching && rm skill.zipInstalls to .claude/skills/epic-caching
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Guide on caching with cachified, SQLite cache, and LRU cache for Epic StackKey capabilities
- →Cache expensive API responses
- →Implement stale-while-revalidate patterns
- →Manage SQLite and LRU cache storage
- →Validate cached data with Zod
How it works
It uses the cachified abstraction to manage cache keys, TTL, and stale-while-revalidate logic across SQLite and in-memory stores.
Inputs & outputs
When to use epic-caching
- →Cache external API responses
- →Implement stale-while-revalidate patterns
- →Optimize expensive database queries
- →Configure SQLite cache storage
About this skill
Epic Stack: Caching
When to use this skill
Use this skill when you need to:
- Cache results of expensive queries
- Cache responses from external APIs
- Optimize performance of data that doesn't change frequently
- Implement stale-while-revalidate
- Manage cache invalidation
- Integrate cache with server timing
Patterns and conventions
Caching Philosophy
Following Epic Web principles:
Weigh the cost-benefit of performance optimizations - Caching adds complexity. Only add cache when there's a clear, measurable benefit. Don't cache "just in case" - cache when you have a real performance problem that caching solves.
When NOT to use cache:
- Data that changes frequently (cache invalidation becomes a problem)
- Data that's already fast to fetch (no measurable benefit)
- Data that's only fetched once (no benefit from caching)
- Simple queries that don't need optimization
- When cache invalidation logic becomes more complex than the problem it solves
Example - Evaluating cost-benefit:
// ✅ Good - Cache expensive external API call
export async function getGitHubEvents({
username,
timings,
}: {
username: string
timings?: Timings
}) {
return await cachified({
key: `github:${username}:events`,
cache,
timings,
getFreshValue: async () => {
// Expensive: External API call, rate limits, network latency
const response = await fetch(
`https://api.github.com/users/${username}/events/public`,
)
return await response.json()
},
checkValue: GitHubEventSchema.array(),
ttl: 1000 * 60 * 60, // 1 hour - reasonable for external data
})
}
// ❌ Avoid - Caching simple, fast database query
export async function getUser({ userId }: { userId: string }) {
// This query is already fast - caching adds complexity without benefit
return await cachified({
key: `user:${userId}`,
cache,
getFreshValue: async () => {
// Simple query, already fast
return await prisma.user.findUnique({
where: { id: userId },
select: { id: true, username: true },
})
},
ttl: 1000 * 60 * 5,
})
// Better: Just query directly without cache
}
Two Types of Cache
Epic Stack provides two types of cache:
-
SQLite Cache - Long-lived, replicated with LiteFS
- Persistent across restarts
- Replicated across all instances
- Ideal for data that changes infrequently
-
LRU Cache - Short-lived, in-memory
- Cleared on restart
- Not replicated (only on current instance)
- Ideal for deduplication and temporary cache
Using cachified
Epic Stack uses @epic-web/cachified as an abstraction for cache management.
Basic import:
import { cachified, cache } from '#app/utils/cache.server.ts'
import { type Timings } from '#app/utils/timing.server.ts'
Basic structure:
export async function getCachedData({
timings,
}: {
timings?: Timings
} = {}) {
return await cachified({
key: 'my-cache-key',
cache,
timings,
getFreshValue: async () => {
// Get fresh data
return await fetchDataFromAPI()
},
checkValue: z.object({
/* schema */
}), // Validation with Zod
ttl: 1000 * 60 * 60 * 24, // 24 hours
staleWhileRevalidate: 1000 * 60 * 60 * 24 * 30, // 30 days
})
}
Cache Keys
Naming conventions:
- Use format:
entity:identifier:data - Examples:
user:${userId}:profilenote:${noteId}:fullapi:github:eventstito:scheduled-events
Avoid:
- Keys that are too long
- Keys with special characters
- Keys that don't clearly identify the content
TTL (Time To Live)
Define TTL:
await cachified({
key: 'my-key',
cache,
getFreshValue: () => fetchData(),
ttl: 1000 * 60 * 60 * 24, // 24 hours in milliseconds
})
Null TTL to never expire:
ttl: null, // Never expires (not recommended unless necessary)
Stale-While-Revalidate (SWR)
SWR allows returning stale data while fresh data is fetched in the background.
Example:
await cachified({
key: 'my-key',
cache,
getFreshValue: () => fetchData(),
ttl: 1000 * 60 * 60 * 24, // 24 hours - after this it's considered stale
staleWhileRevalidate: 1000 * 60 * 60 * 24 * 30, // 30 days - up to here returns stale while revalidating
})
Behavior:
- Less than 24h: Returns cache, no request made
- 24h - 30 days: Returns stale cache immediately, updates in background
- More than 30 days: Waits for fresh data before returning
Validation with Zod
Always validate cached data with Zod:
import { z } from 'zod'
const EventSchema = z.object({
id: z.string(),
title: z.string(),
date: z.string(),
})
export async function getEvents({ timings }: { timings?: Timings } = {}) {
return await cachified({
key: 'events:all',
cache,
timings,
getFreshValue: async () => {
const response = await fetch('https://api.example.com/events')
return await response.json()
},
checkValue: EventSchema.array(), // Validates it's an array of events
ttl: 1000 * 60 * 60 * 24, // 24 hours
})
}
If cached data doesn't pass validation, fresh data is fetched.
Server Timing Integration
Integrate cache with server timing for monitoring:
import { type Timings } from '#app/utils/timing.server.ts'
export async function loader({ request }: Route.LoaderArgs) {
const timings: Timings = {}
const events = await getEvents({ timings })
// Timings are automatically added to headers
return json(
{ events },
{
headers: combineServerTimings(timings),
},
)
}
Cache Invalidation
Invalidate by key:
import { cache } from '#app/utils/cache.server.ts'
await cache.delete('user:123:profile')
Invalidate multiple keys:
// Search and delete matching keys
import { searchCacheKeys } from '#app/utils/cache.server.ts'
const keys = await searchCacheKeys('user:123', 100)
await Promise.all(keys.map((key) => cache.delete(key)))
Invalidate entire SQLite cache:
// Use admin dashboard or
await cache.clear() // If available
Using LRU Cache
For temporary data, use LRU cache directly:
import { lru } from '#app/utils/cache.server.ts'
// LRU cache is useful for:
// - Request deduplication
// - Very temporary cache (< 5 minutes)
// - Data that doesn't need to persist
const cachedValue = lru.get('temp-key')
if (!cachedValue) {
const freshValue = await computeExpensiveValue()
lru.set('temp-key', freshValue, { ttl: 1000 * 60 * 5 }) // 5 minutes
return freshValue
}
return cachedValue
Multi-Region Cache
With LiteFS, SQLite cache is automatically replicated:
Behavior:
- Only the primary instance writes to cache
- Replicas can read from cache
- Writes are automatically synchronized
Best practices:
- Don't assume all writes are immediate
- Use
ensurePrimary()if you need to guarantee writes
import { ensurePrimary } from '#app/utils/litefs.server.ts'
export async function action({ request }: Route.ActionArgs) {
await ensurePrimary() // Ensure we're on primary instance
// Invalidate cache
await cache.delete('my-key')
// ...
}
Error Handling
Handle errors in getFreshValue:
await cachified({
key: 'my-key',
cache,
getFreshValue: async () => {
try {
return await fetchData()
} catch (error) {
console.error('Failed to fetch fresh data:', error)
throw error // Re-throw so cachified handles it
}
},
// If getFreshValue fails and there's stale cache, it returns it
fallbackToCache: true, // Default: true
})
Cache Admin Dashboard
Epic Stack includes a dashboard to manage cache:
Route: /admin/cache
Features:
- View all cache keys
- Search keys
- View details of a key
- Delete keys
- Clear entire cache
Common examples
Example 1: Cache external API response
// app/utils/api.server.ts
import { cachified, cache } from '#app/utils/cache.server.ts'
import { type Timings } from '#app/utils/timing.server.ts'
import { z } from 'zod'
const GitHubEventSchema = z.object({
id: z.string(),
type: z.string(),
actor: z.object({
login: z.string(),
}),
created_at: z.string(),
})
export async function getGitHubEvents({
username,
timings,
}: {
username: string
timings?: Timings
}) {
return await cachified({
key: `github:${username}:events`,
cache,
timings,
getFreshValue: async () => {
const response = await fetch(
`https://api.github.com/users/${username}/events/public`,
)
if (!response.ok) {
throw new Error(`GitHub API error: ${response.statusText}`)
}
const data = await response.json()
return data
},
checkValue: GitHubEventSchema.array(),
ttl: 1000 * 60 * 60, // 1 hour
staleWhileRevalidate: 1000 * 60 * 60 * 24, // 24 hours
})
}
Example 2: Cache Prisma query
// app/utils/user.server.ts
import { cachified, cache } from '#app/utils/cache.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { z } from 'zod'
const UserStatsSchema = z.object({
totalNotes: z.number(),
totalLikes: z.number(),
joinDate: z.string(),
})
export async function getUserStats({
userId,
timings,
}: {
userId: string
timings?: Timings
}) {
return await cachified({
key: `user:${userId}:stats`,
cache,
timings,
getFreshValue: async () => {
const [totalNotes, totalLikes, user] = await Promise.all([
prisma.note.count({ where: { ownerId: userId } }),
prisma.like.count({ where: { userId } }),
prisma.user.findUnique({
where: { id: userId },
select: { createdAt: true },
}),
])
return {
totalNotes,
totalLikes,
joinDate: user?.createdAt.toISOString() ?? '',
}
},
checkValue: UserStatsSchema,
ttl: 1000 * 60 * 5, // 5 minutes
staleWhileRevalidate: 1000 * 60 * 60, // 1 hour
})
}
Example 3: Invalidate cache after mutation
// app/routes/users/$username/notes/new.tsx
export asyn
---
*Content truncated.*
When not to use it
- →Frequently changing data
- →Simple, fast database queries
- →Sensitive data like passwords or tokens
Prerequisites
Limitations
- →Requires clear performance justification
- →Cache invalidation logic can become complex
How it compares
It provides a structured, cost-benefit-driven approach to caching rather than indiscriminate data storage.
Compared to similar skills
epic-caching side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| epic-caching (this skill) | 6 | 6mo | Review | Intermediate |
| bullmq-specialist | 25 | 6mo | No flags | Intermediate |
| rate-limiting-apis | 1 | 25d | Review | Advanced |
| prisma-connection-pool-exhaustion | 1 | 6mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by epicweb-dev
View all by epicweb-dev →You might also like
bullmq-specialist
davila7
BullMQ expert for Redis-backed job queues, background processing, and reliable async execution in Node.js/TypeScript applications. Use when: bullmq, bull queue, redis queue, background job, job queue.
rate-limiting-apis
jeremylongshore
Implement sophisticated rate limiting with sliding windows, token buckets, and quotas. Use when protecting APIs from excessive requests. Trigger with phrases like "add rate limiting", "limit API requests", or "implement rate limits".
prisma-connection-pool-exhaustion
blader
Fix Prisma "Too many connections" and connection pool exhaustion errors in serverless environments (Vercel, AWS Lambda, Netlify). Use when: (1) Error "P2024: Timed out fetching a new connection from the pool", (2) PostgreSQL "too many connections for role", (3) Database works locally but fails in production serverless, (4) Intermittent database timeouts under load.
django-insights
carlosapgomes
Diagnóstico de saúde para projetos Django: performance, segurança e arquitetura.
spark-optimization
wshobson
Optimize Apache Spark jobs with partitioning, caching, shuffle optimization, and memory tuning. Use when improving Spark performance, debugging slow jobs, or scaling data processing pipelines.
fastapi-templates
wshobson
Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.