Redis Caching skill for Codex
Faster reads without stale or leaked data. Codex first checks whether caching is the right fix, then adds cache-aside reads with keys that include every output dimension, jittered TTLs, a token-guarded rebuild lock, short negative caching and invalidation that runs only after the database commits. It sets the eviction policy based on what else lives in Redis and proves the result with hit-rate, stampede and Redis-down tests.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- redis-caching Installs to ~/.agents/skills/redis-caching/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install redis-caching The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project redis-caching Installs to .agents/skills/redis-caching/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/redis-caching.zipunzip -o redis-caching.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) redis-caching PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $redis-caching, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Adds and fixes application caching with Redis or Valkey - cache-aside reads, TTLs with jitter, stampede protection via locks, single-flight and early refresh, stale-while-revalidate, invalidation after commit, versioned keys, negative caching, eviction policy and memory sizing - in Node (ioredis, node-redis) and Python (redis-py). Use when an endpoint or query is slow and read-heavy, cache hit rate is poor, users see stale data after updates, Redis memory spikes, or a cache expiry causes database load spikes.
Use it when
- A read-heavy endpoint or query is slow and tolerates brief staleness
- Cache expiry causes thundering-herd load on the database
- Updates take too long to show up, or never do
- Redis memory, evictions or hit rate look wrong
Use something else when
- You need Redis as a data store with sorted sets or streams: use redis-data-structures
- Caching belongs at the HTTP or CDN layer: use http-caching
- You are designing cache layers across a whole system: use caching-architecture
How to check the result
Measure hit rate and DB queries per request before and after, expire a hot key under load and count loader calls, and stop Redis locally to confirm requests still succeed.
The mistake it prevents
Deleting the cache entry before the database transaction commits, letting a concurrent reader put the old value straight back.
Done well means: the cache is an optimisation the system survives without, hit rate is measured, writes invalidate after the database commits, hot keys never expire all at once, and nobody serves one user’s data to another because of a key that forgot a dimension.
Should this be cached at all?
Cache when all hold: read far more than written (10:1 or more), expensive to compute or
fetch (tens of ms or more), tolerant of bounded staleness, and the key space is reasonably
bounded. Fix the query first if it is slow because of a missing index (postgres-query-tuning):
caching a bad query hides it until the cache is cold.
| Data | Pattern | TTL guide |
|---|---|---|
| Reference data (plans, config, feature catalog) | Cache-aside, long TTL + explicit invalidation | 10 min - 24 h |
| Per-user or per-tenant views | Cache-aside, key includes user/tenant | 30 s - 10 min |
| Expensive aggregates (dashboards, counts) | Stale-while-revalidate or scheduled precompute | 1 - 15 min |
| Third-party API responses | Cache-aside honouring their limits | Per provider terms |
| “Not found” results | Negative cache | 30 - 60 s |
| Rapidly changing data (stock, balances) | Do not cache, or cache under 1 s with care | - |
For HTTP-level and CDN caching use http-caching or cdn-caching; for whole-system cache
layering decisions use caching-architecture.
Before changing anything
- Read
AGENTS.md; find the Redis client and any existing helper (rg -n "redis|ioredis|cache" src). - Check what else the Redis instance holds (sessions, queues, locks, rate limits). It decides the eviction policy.
- Get baseline numbers: endpoint p50/p95 latency, DB query count per request, and
redis-cli INFO stats | grep -E "keyspace_(hits|misses)|evicted_keys".
Key design
<app>:<env>:<entity>:v<schema>:<scope>:<id>[:<variant>]
shop:prod:product:v3:t42:9f1c # tenant 42, product 9f1c, schema version 3
shop:prod:search:v1:t42:sha1(q,filters,page,locale)
- Include every input that changes the output: tenant, user (if personalised), locale, currency, permissions tier, feature flags. Missing one is a data leak, not a perf bug.
- Bump the
v<schema>segment when the cached shape changes; old keys age out by TTL. - Hash long or unbounded inputs; keep keys under ~200 bytes.
Cache-aside with jitter and single-flight (Python)
import json, random, secrets, time
import redis
r = redis.Redis.from_url(REDIS_URL, decode_responses=True)
RELEASE = r.register_script(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
)
def cached(key: str, ttl: int, load, lock_ttl_ms: int = 5000):
raw = r.get(key)
if raw is not None:
return None if raw == "__none__" else json.loads(raw)
token = secrets.token_hex(16)
if r.set(f"lock:{key}", token, nx=True, px=lock_ttl_ms): # one caller rebuilds
try:
value = load()
jittered = int(ttl * random.uniform(0.9, 1.1)) # spread expiries
r.set(key, "__none__" if value is None else json.dumps(value),
ex=60 if value is None else jittered) # short negative cache
return value
finally:
RELEASE(keys=[f"lock:{key}"], args=[token]) # only delete our own lock
for _ in range(20): # others wait briefly
time.sleep(0.05)
raw = r.get(key)
if raw is not None:
return None if raw == "__none__" else json.loads(raw)
return load() # fall through, never hang
On any Redis error, log, count, and call load() directly. A cache outage must degrade to
slower responses, not errors. Set client timeouts low (50-200 ms) so a sick Redis does not
stall every request.
Stampede protection options
| Technique | When |
|---|---|
| TTL jitter (+/-10 %) | Always, for keys written in bulk or at deploy time |
| Lock / single-flight (above) | Expensive rebuilds on hot keys |
| In-process single-flight (one promise per key per instance) | Many concurrent requests in one Node or Go process |
| Early probabilistic refresh (XFetch) | Very hot keys where any miss is costly |
| Stale-while-revalidate | Store {value, fresh_until} with a longer Redis TTL; serve stale and refresh in the background |
| Precompute on schedule | Aggregates that are always needed; cron writes, readers never compute |
Invalidation
- Invalidate after the database transaction commits (post-commit hook, outbox, or CDC). Deleting before commit lets a concurrent reader repopulate the old value.
- Prefer
DEL keyover writing the new value from the write path: two concurrent writers can otherwise leave the older value in cache. - For lists that depend on many rows, use a generation counter:
INCR shop:prod:gen:t42:productsand include the generation in list keys. Old keys expire by TTL. - Never use
KEYS patternin production; it blocks Redis. UseSCANwithMATCHandCOUNT, or better, design keys so pattern deletion is unnecessary. - Multi-instance apps with an in-process L1 cache need pub/sub or RESP3 client tracking to invalidate L1, or a very short L1 TTL (1-5 s).
Memory and eviction
| Redis holds | maxmemory-policy |
|---|---|
| Only cache data | allkeys-lru (or allkeys-lfu for skewed popularity) |
| Cache plus sessions, queues or locks | Separate instance; otherwise volatile-lru and TTL on every cache key |
| Queues or primary data only | noeviction |
- Every cache key gets a TTL. Keys without TTL under
volatile-*can never be evicted. - Find big keys with
redis-cli --bigkeysor--memkeys; values over ~100 KB hurt latency. - Compress large JSON values or cache IDs and hydrate from a smaller cache.
- Changing server config on a managed or production Redis is a production change: confirm with the user first.
Node notes
Use one shared client (ioredis or node-redis), not one per request. Coalesce concurrent
misses in-process with a Map<string, Promise<T>> before touching the distributed lock.
For a lock-free compare-and-delete use the same Lua script as above via eval/defineCommand.
Verification
- Hit rate: compare
keyspace_hits/ (hits+misses) before and after, and per key family via your own metrics (cache_requests_total{family,result}). - Load test the endpoint (
load-testing) with the cache cold and warm; DB QPS should drop roughly by the hit rate. - Stampede test: expire a hot key under concurrent load and confirm only one rebuild runs (count loader calls).
- Staleness test: update a row, then read through the API; the new value appears within the documented bound.
- Kill Redis in a local run: requests still succeed, slower.
Gotchas
- Caching per-user responses under a shared key: the classic cross-user data leak.
- Caching errors or partial results with a long TTL.
- Serialising ORM objects (pickle, class instances) ties the cache to code versions; store plain JSON.
EXPIREreset on every read turns a TTL cache into a never-expiring one for hot keys.- Redis 8 changed licensing (AGPL option) and Valkey is a compatible fork; check which one the platform provides before using version-specific commands.
- For data structure choices (sorted sets, streams, HyperLogLog) use
redis-data-structures; for lock correctness beyond a cache rebuild usedistributed-locks.
Output
Report: what is cached, key format, TTLs, invalidation trigger per key family, stampede protection used, eviction policy, measured hit rate and latency before and after.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Redis Caching"
short_description: "Faster reads without stale or leaked data"
default_prompt: "$redis-caching Cache the product catalog endpoint in Redis without serving stale prices" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.