Caching a page is easy.
Knowing whether you should cache it is harder.
Knowing when that cached page stopped being true is harder still.
Most response caches ultimately ask the application developer to answer both questions: decide which responses are safe to share, construct the right cache key, and remember which entries need to disappear when the underlying application changes.
Forget one dependency and the cache doesn't necessarily fail.
It does something worse.
It keeps working.
It just serves the wrong page.
Suprnova RenderCache takes a different approach.
RenderCache is Suprnova's framework-level HTTP response cache. It can store a proven-safe response from a GET or HEAD route and serve the next matching request without running your handler at all.
But the important part isn't that it caches responses.
It's that Suprnova watches the render happen.
Opt in. Then prove it.
RenderCache is explicit.
Nothing is cached until you opt a route or group into it.
router.try_render_cache?;
That declaration says /blog is intended to be a publicly shared representation with a particular freshness policy.
But declaring something cacheable doesn't make it so.
When the handler runs, RenderCache observes what happens.
Did the render read the session?
Did it depend on the signed-in user?
Did it use the negotiated locale?
Did authorization depend on a principal?
Did a model scope depend on request-specific state?
Did the handler use raw SQL whose dependencies the framework cannot identify?
RenderCache compares what the route declared with what the render actually did.
If those two stories don't agree, the response isn't stored.
The visitor still gets the correct page.
The cache simply declines it.
Failure means rendering normally
That behavior is fundamental to the design.
Opting a route into RenderCache isn't a promise that every request will produce a cache entry.
It's a request to cache the response when Suprnova can prove that doing so is safe.
Suppose /blog reads a value from the visitor's session to display a flash message.
That means the resulting page depends on state that isn't safe to put into the shared representation.
RenderCache sees the session read.
The response becomes uncacheable.
Nothing is stored.
And /blog continues working exactly as it did before RenderCache existed.
Caching becomes an optimization rather than a new correctness requirement.
The key is a declaration
A cached route doesn't really store "a page."
It stores a representation.
Its lookup key is derived from things the route explicitly declares: the route pattern, path parameters, declared query parameters, declared variance dimensions, the application build, and the current cache authority epoch.
If your output depends on locale, declare locale.
If it depends on the current principal, declare principal.
If page and sort change the response, declare those query parameters.
If an unexpected query parameter arrives, RenderCache doesn't quietly pretend it doesn't matter.
That request bypasses the cache.
The philosophy is deliberately conservative:
when Suprnova can't prove two requests should receive the same representation, it doesn't make them share one.
The RenderCache Representations manual goes deeper into what is actually stored, how lookup keys are constructed, public and private representations, conditional requests, freshness states, and Live shell stitching.
Cache invalidation without forget
Then there is the other half of caching.
Invalidation.
Most caches expire eventually. RenderCache does too, but expiration is the backstop rather than the primary mechanism.
RenderCache uses generations.
While a response renders, Suprnova records the data dependencies it can identify.
A render that loads Todo::all(), for example, depends on the todos table.
The cached representation stores the generation it observed.
Later, your application writes a new Todo:
todo.save.await?;
That ordinary database write advances the relevant generation.
The next request can now prove that the stored representation was produced against an older version of the data.
No cache key was named.
No forget() call was made.
No model observer needed a hand-maintained list of pages to invalidate.
The write is the invalidation.
The RenderCache Generations manual covers this mechanism in depth, including dependency tracking, invalidation granularity, coherence checks, rebuild coordination, stale serving, and what happens when data changes while a response is being rendered.
A hit skips the application work
When a stored representation is still current, RenderCache can answer the request without running the handler.
That means:
no handler,
no ORM query,
no template render,
no serializer,
and no body copy inside the cache engine.
The response already exists.
RenderCache only needs to establish that the representation is still current and serve it.
The result is caching at the level that actually matters to the request: the completed HTTP response.
Fresh doesn't have to mean all or nothing
Applications also have to decide what happens during a rebuild.
RenderCache models four freshness states:
Fresh responses are served immediately.
Stale-servable responses can be returned immediately with a Warning header while one background rebuild refreshes the representation.
Stale-on-error responses trigger a foreground rebuild but can fall back to the stored representation if that rebuild fails.
Dead representations aren't served at all. The request renders again.
That gives an application control over the tradeoff between freshness, latency, and availability without turning those decisions into ad hoc cache logic throughout the codebase.
These states, along with ETag, Cache-Control, Vary, Age, conditional requests, and the exact shape of a stored entry, are covered in RenderCache Representations.
One rebuild, not a stampede
When several requests discover the same missing or outdated representation at once, RenderCache coordinates the rebuild.
One request becomes the leader.
It renders and may publish the new representation.
Requests already waiting for that key wait for the leader and then re-evaluate what was published.
And the waiter count is bounded, so an overloaded key can't create an unlimited queue.
Across multiple nodes, duplicate computation can still happen.
Conflicting publication cannot.
Rebuild leases and publication fencing make sure an older worker cannot overwrite newer authority.
The mechanics behind singleflight, waiters, publication fencing, and serving stale representations during rebuilds are documented in RenderCache Generations.
Public, private, and somewhere in between
Not every cacheable page is public.
RenderCache supports different representation classes.
PublicShared is the familiar case: one representation can be shared by everyone matching its declared variance.
PrivateCached stores representations partitioned by principal or tenant.
And then there is PublicShellStitched.
This one exists specifically for Suprnova Live.
Imagine a dashboard whose navigation, layout, headings, and surrounding document are identical for everyone—but whose Live islands belong to the signed-in user.
Caching the whole document would be unsafe.
Rendering the whole document every time throws away everything that could have been shared.
RenderCache can instead store the shared shell with typed holes where the private islands belong.
On a hit, the shell is reused while the Live islands are mounted again under the authority of the current request.
The shared bytes are cached.
The private bytes never enter the shared representation.
A page doesn't have to choose between cache everything and cache nothing.
The full representation model—including Complete, Composite, PrivateCached, and PublicShellStitched entries—is described in RenderCache Representations.
Memory, files, databases, or Redis
RenderCache starts small.
Its embedded profile can operate in-process, with an optional file-backed tier for representations that should survive a restart.
When an application grows to several nodes, RenderCache can move its shared storage and rebuild coordination to the database.
Or it can use Redis when latency matters more than durability.
The application-facing model doesn't change.
The same routes, policies, keys, dependency collection, and middleware flow remain in place.
More importantly, the cache tier is never the authority for whether something is current.
The database-backed generation ledger is.
Redis can disappear.
Files can be evicted.
A stored representation can be lost.
Those events cost performance.
They don't make stale content true again.
The RenderCache Deployment manual covers the embedded, database, and redis profiles, storage tiers, rebuild coordination, environment configuration, migrations, and multi-node deployment.
You can see what the cache is doing
Invisible caching is difficult to trust, so RenderCache also has an operational surface.
Cached responses expose standard HTTP behavior including ETag, Cache-Control, Vary, Age, and stale warnings where appropriate.
Telemetry distinguishes hits, misses, bypasses, stale responses, moved representations, and—importantly—declined renders.
When a route you expected to cache doesn't, the framework can tell you why.
RenderCache also provides body-free inspection of cached entry metadata.
And when something has gone badly wrong, an authority epoch can be advanced to make every previous cache key unreachable immediately without enumerating and deleting entries one at a time.
The cache has an emergency lever.
It doesn't share that lever with your sessions, queues, or arbitrary application cache.
For production diagnostics, telemetry, cache inspection, testing strategies, disk hygiene, emergency invalidation, and recovery procedures, see RenderCache Operations.
RenderCache isn't Cache
Suprnova still has a normal key-value cache.
suprnova::Cache is what you use when you know you have a value you want to compute once and reuse.
You choose the key.
You put the value in.
You decide when to remove it.
RenderCache operates at a different level.
You don't call it from your handler.
It caches completed HTTP representations.
It derives keys from route declarations.
It observes what the render depended on.
And ordinary application writes move the generations that determine whether those representations are still current.
Use Cache when you want to cache a value.
Use RenderCache when you want Suprnova to avoid doing the request again.
Caching should be an optimization you can trust
Response caching has always offered an attractive bargain:
do less work and serve pages faster.
The uncomfortable part is what developers traditionally have to put on the other side of that bargain.
Did we build the right key?
Did we include every dimension?
Did we remember every invalidation path?
Can these two users actually share this response?
Did somebody add a session read six months after this route was declared cacheable?
RenderCache can't make every response cacheable.
It deliberately refuses to.
Instead, it tries to make the responses it does store defensible.
The route declares what should be true.
The render records what actually happened.
The framework compares the two.
The data itself tells the cache when it changed.
And when Suprnova can't account for the safety of a representation, it renders the page normally.
Cache what you can prove. Render everything else.
Go deeper
- RenderCache Representations — representations, keys, storage layers, freshness, private caching, and Live shell stitching.
- RenderCache Generations — dependency tracking, automatic invalidation, coherence, and rebuilds.
- RenderCache Deployment — embedded, database, and Redis profiles and multi-node deployment.
- RenderCache Operations — telemetry, inspection, testing, recovery, and emergency invalidation.


Comments 0