Caching Strategy: What Is Actually Cached
smo1 intermediate 7 min read
ELI5
SMO1’s code has places to keep copies of link data close to the visitor, but the most important one, the edge copy, is switched off. Every short-link visit asks the API, and the API asks the database. A small cache inside the API exists, but it only helps a side route that real visitors rarely use. The dashboard keeps its own short-lived copies in the browser.
Technical Deep Dive
Source: zoomies-edge wrangler.toml:50-63, src/handlers/redirect.ts:83-121, src/index.ts:236-256 (origin/main 5bdde59); purr-api internal/service/redirect_cache.go:50-117, internal/service/link_service.go:183-187, 376, 416, 425-470, internal/database/redis.go:89-111, 142-149, internal/service/kv_sync_service.go:89-138 (bb6cfc2); meow-web src/components/providers.tsx:30, src/lib/auth.ts:49, src/lib/airlock.ts:38-65 (e5d808c), read 2026-10-25.
Layers as coded and as deployed
flowchart TB subgraph Browser["meow-web browser tab"] RQ["React Query, staleTime 60 s default, 5 min for the session query"] end subgraph MeowServer["meow-web server"] JW["JWKS in module memory, 5 min"] end subgraph Edge["zoomies-edge"] KV["LINKS_CACHE KV: NOT BOUND in lion"] end subgraph Purr["purr-api"] RD["Redis link:v3:id, 5 min, only for /:identifier"] RL["Redis rate:key:window counters"] end PG[(Postgres)] RQ -->|"/api/purr"| Purr KV -.->|"skipped"| Purr Edge -->|"/api/links/slug and /api/links/resolve"| PG RD --> PG| Layer | Where | Key and TTL | Production status |
|---|---|---|---|
| React Query | meow-web browser | staleTime 60 s by default (providers.tsx:30), 5 min for the auth query, 0 on billing | active |
| JWKS cache | meow-web server memory | Airlock JWKS, 5 min (airlock.ts:38-65) | active |
| Edge KV | Cloudflare KV LINKS_CACHE | link:{host}:{slug} (redirect) or link:{slug} (proxy probe), 300 s | off: binding commented out |
| Redis redirect cache | purr-api | link:v3:{identifier}, 5 min (redis.go:144-149) | active, but only on purr-api’s own GET /:identifier route |
| Postgres | Railway | source of truth | always hit by the edge path |
Why the Redis cache does not help smo1.io redirects
ResolveForRedirect, the only reader of the Redis cache (link_service.go:445-470), backs purr-api’s own GET /:identifier and /@:username/:slug routes. The Worker calls /api/links/slug/:slug (LookupBySlug) and /api/links/resolve (ResolveByHostAndSlug), which go straight to the repository (link_service.go:425-437). So the visitor path has no cache between the edge and Postgres.
The Redis cache is careful where it does run: it stores only links AuthorizeRedirect allows, never protected, paused or expired ones; it never caches a custom-domain link under its bare slug, only under its globally unique uid (redirect_cache.go:50-69); and update and delete call invalidateRedirect for the uid plus old and new slugs (link_service.go:376, 416).
Why the edge KV is off
purr-api’s KV sync writes keys named by the bare uid and custom slug, with a camelCase value {url, isActive, expiresAt, utm*} (kv_sync_service.go:38-49, 124-133). The Worker reads link:{host}:{slug} and expects the snake_case LinkResponse shape with destination, is_active, protection_type and redirect_mode. Neither keys nor values meet. wrangler.toml:50-55 records that enabling the binding as-is “would be a regression”. See smo1-015.
Rate-limit counters
Redis also holds fixed-window counters rate:{identifier}:{window} (redis.go:89-111). The edge has its own per-IP counter in the Cloudflare Cache API, 120 per minute (smo1-011).
Key Terms
- LINKS_CACHE — the KV binding both edge lookups support; not bound in production
- link:v3: — purr-api’s Redis key prefix for the redirect cache
- ResolveForRedirect — the only Redis-cached lookup, behind purr-api’s own redirect routes
- invalidateRedirect — deletes Redis entries for a link’s uid and slugs on update or delete
Q&A
Q: Name all four cache layers in order of closeness to the user. A: In the code: React Query in the browser, Cloudflare KV at the edge, the Redis redirect cache in purr-api, then Postgres as the source of truth (plus meow-web’s server-side JWKS cache, which is not a link cache). In production the KV layer is off and the Redis layer is not on the edge path.
Q: What is the TTL for KV link entries written by the edge worker?
A: 300 seconds (expirationTtl: 300, redirect.ts:117) if it were bound. With LINKS_CACHE absent nothing is written.
Q: Which cache layer is invalidated when a link is updated?
A: Redis: UpdateLink and DeleteLink call invalidateRedirect for the uid and slugs. The KV sync also re-puts or deletes its bare-slug keys when KV credentials are configured, but the Worker never reads those keys.
Q: Does any cache sit between a smo1.io redirect and Postgres today? A: No. Both edge lookups go to purr-api routes that read Postgres directly.
Examples
An owner changes a link’s destination. Redis entries for its uid and slug are deleted, purr-api’s own /:uid route re-reads Postgres on the next hit, and smo1.io/<slug> serves the new destination on the very next click because nothing at the edge held the old one. The flip side: a purr-api or Postgres outage takes every redirect down with it.
neighbors on the map
- Edge Redirect Flow debugging why a slug does not redirect
- Click Tracking Pipeline debugging missing or duplicate click counts
- Database Architecture writing a migration or a production SQL check
- Rate Limiting: Edge per-IP, Origin per-Tier debugging 429 errors for specific users