Every system has a breaking point. The question is whether you’ll see it coming. At Gray Haven Lab, we spend a lot of time thinking about the boundary between what a machine can handle gracefully and what turns into a 3 a.m. phone call. That boundary often comes down to three decisions: what you store in advance, what you calculate on the fly, and what you treat as a genuine emergency.
These aren’t just architectural choices. They’re operational habits. Get them right, and your infrastructure breathes. Get them wrong, and you’re fighting fires that should have been smoldering embers.
The Cache Is Not a Storage Closet
Caching is the art of remembering just enough to avoid repeating expensive work. But a cache is not a database. It’s not a permanent home for data. It’s a temporary shortcut, and treating it otherwise leads to stale results, memory bloat, and a false sense of resilience.
We see teams cache everything “just in case.” User sessions, rendered pages, API responses, even configuration files. The logic is seductive: if it’s in memory, it’s fast. But speed without strategy is just noise. A bloated cache increases eviction pressure, hides source-of-truth drift, and makes cold starts punishingly slow.
At Gray Haven, we cache what is expensive to compute and safe to lose. Think of a precomputed report that aggregates thousands of log lines. If the cache disappears, the system recomputes it—slowly, but correctly. We don’t cache anything that, if served stale, would corrupt a downstream decision. A good rule: if you can’t tolerate serving a version that’s five minutes old, don’t cache it. Instead, compute it fresh and make the computation fast.
We also distinguish between lookaside and inline caches. A lookaside cache sits to the side of a primary data store; the application checks it first, then falls back. An inline cache sits directly in the request path, often as a reverse proxy or CDN. Inline caches are great for static assets and fully precomputed pages. Lookaside caches work for partial results, like user permissions or feature flags. The failure mode matters: an inline cache outage can block all traffic; a lookaside cache outage just slows things down. We design for the failure mode, not just the happy path.

Compute When Consistency Is Non-Negotiable
Some things should never be cached. Financial balances, inventory counts during a flash sale, authentication tokens—these demand real-time accuracy. The cost of recomputing them is trivial compared to the cost of serving wrong data.
But “compute” doesn’t mean “do everything from scratch.” It means derive the answer from the source of truth at the moment it’s needed. That derivation can be optimized. Pre-warming, lazy evaluation, and incremental computation all reduce latency without sacrificing correctness. The key is that the answer is always fresh, and the system’s state is always consistent.
We often see teams over-caching because they’re afraid of their own databases. A query that takes 800ms is a problem, but the solution isn’t to cache the result for an hour. It’s to make the query faster. Indexes, materialized views, read replicas—these are compute-side optimizations that preserve correctness. Caching is a bandage. Compute optimization is surgery.
There’s also a middle ground: deterministic recomputation. If a result can be derived purely from inputs that are already available, and the derivation is cheap, don’t cache the result. Cache the inputs if you must, but recompute the answer. This keeps the system’s surface area small and its behavior predictable. We lean on this pattern heavily for configuration-derived values: feature flags, routing rules, and rate limits.
What Deserves a Panic
Not every outage is a crisis. Not every spike is a disaster. Panic is a resource, and like any resource, it should be spent carefully. At Gray Haven, we classify incidents into three tiers: degraded, broken, and dangerous.
Degraded means slower responses, stale caches, or a non-critical feature that’s offline. It’s uncomfortable but not urgent. Broken means a core function is unavailable—users can’t log in, payments fail, data is lost. That’s when you wake someone up. Dangerous means the system is actively corrupting data, leaking secrets, or opening security holes. That’s when you drop everything.
Panic should be reserved for dangerous and, in some cases, broken states. But many teams panic over degraded states. They restart services, flush caches, and roll back configs—often making things worse. A calm, practiced response to degradation prevents it from becoming broken. That’s why we wrote the Recovery Checklist Before You Write It. It’s a pre-built decision tree for common failure modes, so you don’t have to think clearly while your pager is screaming.
One of the most dangerous panic triggers is the cache stampede. A popular cache key expires, and suddenly hundreds of requests hit the origin simultaneously. If the origin is already strained, it collapses. The fix isn’t to panic-flush the cache or reboot servers. It’s to use locking, request coalescing, or probabilistic early recomputation. These are patterns you implement before the stampede, not during it.

Designing the Boundary
The line between cache and compute isn’t fixed. It shifts with load, data freshness requirements, and the cost of mistakes. We treat it as a dial, not a switch. During normal operations, we cache aggressively to reduce latency and database load. During an incident, we may dial caching down or disable it entirely to eliminate a source of stale data. This requires instrumentation: cache hit rates, recompute latencies, and error budgets per endpoint.
We also design for graceful degradation. If the recompute path is too slow, the system can fall back to a stale cache entry—but only if the entry is explicitly marked as safe for staleness. A stock ticker can be five minutes old. A password reset token cannot. These decisions are made at design time, not during the outage.
Another boundary is cache invalidation. It’s famously one of the hard problems in computer science, but it’s manageable if you limit scope. We avoid global invalidations. Instead, we use targeted invalidation by key prefix, versioned cache namespaces, or time-to-live values that reflect the actual rate of change in the underlying data. If a dataset changes every 30 seconds, a 60-second TTL is a lie. A 10-second TTL is honest.
Operational Patterns We Trust
Over years of incident response, we’ve settled on a few patterns that reduce the surface area for panic:
- Cache budgets: Assign a maximum memory footprint per service. When the budget is exceeded, evict the least valuable entries based on access frequency and recompute cost—not just time.
- Recompute budgets: Set a maximum latency for recomputation. If a query can’t be optimized below that threshold, it becomes a candidate for caching, but only with a strict freshness policy.
- Panic budgets: Define how many incidents per quarter justify a full-scale war room. If you’re exceeding that budget, your architecture—not your alerting—needs work.
These aren’t hard rules. They’re heuristics that force teams to confront the real cost of their decisions. A cache that saves 50ms but causes three outages a year is a bad trade. A recompute path that takes 200ms but never fails is a good one. Measure the trade, not just the latency.
When the Cache Becomes the Source of Truth
One of the most dangerous anti-patterns we see is the “cache-as-database” drift. It starts innocently: a team caches API responses to reduce load on an upstream service. Then the upstream service changes its schema. The cache still holds old-format data. Consumers adapt to the cached format. Over time, the cache becomes the de facto contract, and the upstream service is afraid to change anything because it might break consumers that rely on the cached shape.
This is a compute problem disguised as a cache problem. The fix is to treat the cache as an opaque acceleration layer. Consumers must always be able to tolerate a cache miss that falls through to the source of truth. If they can’t, the cache has become a critical dependency, and it needs the same rigor as a database: schema versioning, migration plans, and monitoring for drift.
We’ve also seen the inverse: teams that refuse to cache because “caches cause bugs.” They recompute everything, hammering their databases until response times climb and customers leave. This is a failure to recognize that latency is a bug. A system that is correct but unusably slow is still broken. The answer isn’t to avoid caches; it’s to cache with discipline.

Incident Posture: Calm, Curious, and Specific
When an incident does escalate, the difference between a short outage and a multi-hour nightmare often comes down to posture. We train teams to approach broken systems with curiosity, not fear. The first question isn’t “How do we fix this?” It’s “What changed?” and “What is the actual user impact?”
Cache-related incidents have a distinct smell. Metrics show cache hit rates dropping while origin load spikes. Latency increases, but error rates may stay flat—at first. The instinct is to dump the cache and hope the problem disappears. But dumping the cache during a load spike is like throwing gasoline on a fire: you’ve just guaranteed that every request hits the origin cold. Instead, we extend TTLs on stable entries, shed non-critical traffic, and spin up additional origin capacity if available.
Compute-related incidents look different. CPU saturates, queues build, and timeouts cascade. Here, the instinct is often to add more compute—scale out, scale up. But if the bottleneck is a lock or a sequential scan, more compute just adds more contention. The fix is to reduce the work per request: simplify queries, batch writes, or shed load aggressively. Sometimes the right move is to serve stale data from a cache that you previously decided not to use. That’s an operational decision, not an architectural one, and it requires having the cache available as a circuit breaker.
We practice these scenarios. Not in production, but in tabletop exercises where we walk through failure modes and decide, in advance, which levers we’ll pull. This is where the Recovery Checklist Before You Write It earns its keep. A checklist written during calm hours prevents flailing during stressful ones.
Building Resilience Into the Daily Rhythm
Resilience isn’t a feature you add in a sprint. It’s a property that emerges from hundreds of small decisions: how you set TTLs, how you structure retries, how you log errors, how you review incidents. We bake these decisions into code review checklists, deployment runbooks, and architecture decision records.
For every new endpoint, we ask:
- What is the freshness requirement? If the answer is “within 1 second,” caching is probably off the table.
- What is the recompute cost? If it’s high, we look for deterministic recomputation or precomputation opportunities.
- What is the failure mode? If the recompute path fails, can we serve a stale cached result? If not, what’s the user experience?
- What is the panic threshold? At what point does slowness or staleness become an incident? We define that number and alert on it.
These questions force clarity. They turn vague fears into measurable thresholds. And they make the system’s behavior predictable—not just for users, but for the engineers who operate it.
FAQ
How do I decide what to cache and what to compute in real time?
Start by measuring the recompute cost and the staleness tolerance for each piece of data. If recompute is cheap and staleness is unacceptable, compute it live. If recompute is expensive and staleness is tolerable, cache it with a TTL that matches the data’s natural rate of change. If recompute is expensive and staleness is unacceptable, invest in faster recompute paths—indexes, precomputation, or incremental updates—before resorting to caching.
What’s the biggest mistake teams make with caching?
Treating the cache as a source of truth. When downstream systems start depending on cached data formats or freshness guarantees that the cache can’t honor, you’ve created a fragile coupling. The cache should always be an acceleration layer that can be flushed or bypassed without breaking correctness. If you can’t safely flush your cache in production, you don’t have a cache—you have an unversioned, undocumented database.
How do I know if my team is panicking too much?
Track the ratio of “degraded” alerts to “broken” alerts. If every degraded state triggers a war room, your alerting thresholds are too tight, or your team lacks confidence in the system’s ability to self-heal. Invest in graceful degradation patterns and practice incident response scenarios. A team that panics at every spike can’t think clearly when a real crisis hits.
Should I cache at the edge, in the application, or in the database?
It depends on what you’re caching and who consumes it. Edge caches (CDNs) work best for static assets and fully rendered pages that are the same for all users. Application caches (in-memory stores like Redis) work for computed results that are shared across requests but may vary by user or session. Database caches (materialized views, buffer pools) work for query results that are expensive to recompute but need to stay consistent with the underlying data. Use the right layer for the right data, and don’t cache the same thing in multiple layers without a clear invalidation strategy.
What’s the first thing to check during a cache-related incident?
Check cache hit rates and eviction rates. A sudden drop in hit rate often means a mass expiration or a key-space change. Check if a deployment changed cache key formats or TTLs. Check if an upstream data source changed in a way that invalidated cached entries. And before you flush the cache, check origin capacity—flushing during a load spike can cause a stampede that makes the outage worse.
Resilience isn’t about never failing. It’s about failing in ways you’ve already imagined, with responses you’ve already practiced. Cache what’s expensive and safe to lose. Compute what must be right. And panic only when the data is bleeding.









