Multitenant serving and cache invalidation

Jesse James Richard
|
|
7 min read
#Method
#Architecture

Giant Context hosts the websites it builds, so a request for a customer's own domain has to arrive at an address the platform owns, come back as that customer's site and no one else's, and be current.

The connection itself is a customer pointing their DNS at a load balancer IP the settings dialog gives them, after which the platform manages the certificate.

Below is what a request does on the way in, and what a save does to the copy that request was served from.

A screenshot of a dark-themed user interface showing DNS configuration settings for a domain.
The domain dialog shipped yesterday. One load balancer IP, and a managed certificate the platform handles.

How a hostname finds its site

Every connected domain resolves to the same load balancer IP. The platform has one address, not one per site, and adding a customer adds a DNS record on their side rather than infrastructure on mine.

Which means the address cannot tell the request apart. The hostname does that. Past the load balancer the request reaches the router, a small service that reads the Host header, looks it up against the domains customers have connected, and resolves it to one site.

That lookup is the tenant boundary. A hostname nobody has connected resolves to nothing, and there is no request shape that reaches a site the hostname does not own. Nothing further downstream has to re-check which tenant it is serving, because by then the question is settled.

Nothing in the response identifies the platform either. The visitor is on the customer's domain, with the customer's certificate, looking at the customer's brand.

The page was rendered before the request

The public sites are served by Next.js, the one place in the stack where I kept a framework instead of building my own. A page is not rendered when a visitor asks for it. It was rendered earlier and stored, the visitor gets the stored copy, and the framework re-renders in the background at most once a minute while anyone is still asking.

That matters more here than it would on a single site, because the rendering layer sits behind a proxy, and the proxy adds latency before a byte of page moves. Serving from the store is what pays that back. A request that misses the store pays the full render, the database read and the proxy hop together, and it is the difference between a page that feels hosted and a page that feels generated.

Getting there was a run of small correctness problems rather than one hard one. Content decoding failures on the proxy the week before (bd21f1072). Header forwarding that dropped the hostname the router needs. A certificate stuck reporting the wrong state, fixed the same day the domain dialog shipped (86ad7bbe8). Each of those is a twenty-minute fix, and each one breaks the only route a visitor has to the site, which is the whole category of bug this layer produces.

Purging on save

The only thing that can go wrong with a stored page is the site changing after it was stored. The customer opens the console, edits the headline, clicks save, and the copy sitting in the store is now a page that no longer exists anywhere except in the store, served just as fast as before.

So saving does one more thing. The console calls a revalidation route on the rendering layer, which drops the stored copy for that path, and the next request re-renders from what the API now says. No polling, no expiry to wait out, no window where the site is a version behind.

That wire has been in place since the first week the platform could serve a site at all (f9e56320d), which was before there was much of a site to serve. A cache with no purge keeps returning yesterday's page and reporting success, so nothing in the logs says anything is wrong, and it is easier to build the purge first than to retrofit it after a customer has found the gap.

The next request

The next visitor types the same domain. The router makes the same lookup, the cache has no copy, the page renders fresh, and the new headline is there. From here it is cached again, fast again, until the next save.

Everything else the platform does, the editor, the AI, the permissions, ends up funnelling into that round trip. A person types a domain and sees the current site quickly, or none of the rest of it counts.

One region, and what comes after

The stored copies live with the rendering service, in one region. A visitor in the same region gets them quickly. A visitor on the other side of the world pays the distance twice, once to reach the load balancer and once for the page to come back, and no amount of caching in one place fixes that.

The answer is copies closer to the visitor, whether that is a CDN in front of the rendering service or the render itself running at the edge. I have not built either, and I keep thinking about it, because the expensive half is already done.

Distributing a cache is easy until something changes and you have to reach every copy. That problem is the one the save wire already solves. The console knows the moment a page stops being true and it already emits a purge for that exact path. Pointing that at a distribution network instead of at one rendering service is a change of target, not a change of design.

Which is the argument for building invalidation before you need it. It was a small piece of work in December on a platform with one site, and it is the reason a much larger change stays available.

The frontend keeps growing

Every feature the platform grows has to exist out here too. New dialogs, headers, sidebars, localization, cookie banners, all of it rendered for strangers and matched to whatever the console builds. So the serving layer is never finished, and it deploys only when its own files change, the same rule every service follows, so each deploy is scoped and proven.

Wiring this up yourself

One address. A load balancer with one IP that every customer domain points at, and a certificate story automated from day one, because certificates at any scale are not a hand process. On Google Cloud that is Cloud Load Balancing with managed certificates.

One lookup. A small routing service that maps hostname to tenant to site. Keep it boring and keep it fast, and treat an unknown hostname as nothing rather than an error page that admits a platform is back there.

One cache with a kill switch. Incremental Static Regeneration gives you the cached copies and the background re-render. What you have to build is the wire from the save button to the revalidation, because a cache with no way to purge it will serve a stale page for as long as the expiry allows and look healthy doing it.

The routing takes about a week. Trusting the cache took longer, and the difference between the two is where the work actually was.

#Method
#Architecture
#AI & Agents

Finding the name is how I work out whether I understand the thing I am about to build. A name draws a boundary before any code exists, then steers eve...

Jesse James Richard

|

Feb 17, 2026
Read previous

Hiring an early engineer, or building something like this

Remote, Pacific time, full-time or contract. Get in touch.

Contact Jesse
Home
About
Contact
Sitemap
Privacy Policy
Terms of Service
Cookie Policy
Multitenant serving and cache invalidation | Jesse James Richard