A multi-tenant PaaS or web hosting platform concentrates a large number of hostnames and requests at one entry point.
A lightweight Worker alone is not enough: KV reads, static file retrieval, cache propagation, and unknown hosts all have to be designed as one request path.
At the public edge of KamuiDash, a Cloudflare Worker resolves a tenant application from the Host. It then sends the request to either a dynamic application or the static delivery path. The broader architecture is covered in Designing a Multi-Tenant PaaS with Cloudflare at the Edge.
This article focuses on reducing the KV reads generated at that shared entry point. The goal was to stop asking KV the same question on every request and briefly share resolved routing decisions within the same Cloudflare data center.
The first problem: correct, but read every time
The initial routing model was deliberately simple. A custom domain required one lookup to resolve the internal application, followed by another lookup for its delivery information. Platform subdomains also used KV to resolve dynamic or static routing data.
This is easy to reason about and works at low traffic. At PaaS scale, however, unchanged routing information is read repeatedly while applications remain untouched.
1. Do not look up platform-owned Hosts in the domain KV
The first reduction was to stop asking KV for facts already encoded in the hostname. The platform apex and wildcard subdomains are already internal application names. Only a true custom domain needs translation from its public Host to an internal Host.
2. Consolidate dynamic and static routing records
Dynamic applications and static sites previously used separate routing maps. We merged them into one routing KV so a single application lookup determines the delivery type and returns the data needed for that path.
in one record
During migration, we copied records into the new KV, checked counts and duplicates, and then switched the Worker. We intentionally removed the old read fallback: leaving two sources of truth in place makes later incidents harder to diagnose.
3. Check the static body cache before KV
If the same static URL is requested repeatedly, its cached response can be returned before resolving the Host. We therefore moved the public-URL content cache ahead of all routing lookups.
- Resolve Host
- Read routing KV
- Identify static route
- Check body cache
- Retrieve static file
- Check body cache
- Resolve Host
- Read routing KV
- Retrieve static file
The shared cache can contain unrelated responses, so entries written by this Worker carry a private marker. The Worker verifies the marker on a HIT and removes it before returning the response. The same header is also stripped from downstream metadata so internal state never leaks to users.
4. Share the resolved route in Route Cache
Beyond static response bodies, we store the result from Host resolution through the validated routing record in the Cache API. Dynamic response bodies are never shared; only their route is cached.
The cache key is derived from a normalized Host and kept within a platform-controlled scope. Concrete internal paths and identifiers are intentionally omitted.
Only records whose state and delivery data pass validation are stored. Missing fields do not fall back to an implicit destination; routing fails closed instead. Internal field names and identifiers are omitted.
5. Briefly cache confirmed not-found results
If only valid routes are cached, repeated lookups for the same missing Host still reach KV. Cloudflare bills KV operations even when a key does not exist.
We therefore negative-cache only authoritative not-found results for a short period. In-progress states, malformed records, and upstream failures are not cached.
The tradeoff is that a Host accessed before registration may continue returning 404 briefly after it is created. Keeping the Route Cache and KV edge-cache freshness budgets aligned avoids materially increasing the existing propagation window.
A shorter TTL is not automatically better
TTL should be chosen from the acceptable time for a newly deployed route or asset to become visible, not from cost alone. We treat KV edge caching, Route Cache, and static body caching as a single freshness budget rather than extending each independently. Concrete values are intentionally not published.
Cache API and Workers Caching are different
This design uses the Cache API from inside Worker code. Cloudflare also offers Workers Caching, which can return a response before the Worker executes; the two mechanisms are independent.
- Worker request is billed
- Worker CPU executes
- KV and downstream reads are skipped on HIT
- Worker request is billed
- Worker CPU does not execute
- KV and downstream work are skipped
The current Worker is a shared gateway for static and dynamic routes. Dynamic responses default to private, so they are not stored in Workers Caching. With routing lookups as the immediate cost driver, Route Cache was the first priority.
Validate data on both sides of the cache
Caching accelerates correct values and incorrect values alike. Records are therefore validated before storage and again after retrieval.
- Validate the schema generation — Do not silently interpret a future record format.
- Cache only valid states as routes — In-progress records are not cached as healthy routes.
- Require all delivery data — Missing values never select an implicit destination.
- Do not expose internal markers — Strip the same header even when it arrives from downstream.
- Do not share dynamic responses — Use
Cache-Control: privateby default. - Do not reflect a user path in a 404 — Return a fixed message.
Roll out through development first
The router affects every tenant, so we verified locally, then in development, and finally in production. Every Terraform plan had to show only an in-place Worker update, without unrelated changes to KV, static delivery, DNS, or Tunnel resources.
- 01Local testsHealthy, 404, in-progress, malformed, and cache errors
- 02Development planOnly the intended in-place update
- 03Development URLsDynamic, static, and unknown Hosts
- 04Production planReconfirm the isolated Worker update
- 05Production URLsPlatform, custom, static, and 404 routes
Local coverage included platform Hosts, custom domains, dynamic and static routes, positive and negative cache results, in-progress states, malformed records, and KV misses. Production verification used real URLs for the same public behaviors.
What remains
The main routing-KV reductions are now in place. The next optimizations should be driven by measurements.
- Sample successful logs — At high traffic, observability volume becomes a cost of its own.
- Purge explicitly on updates — Improve cache efficiency without extending visible propagation time.
- Use layered protection for abnormal traffic — Protect resources at the edge instead of relying on cache behavior alone.
Summary
The important change was not adding a single cache. It was putting the cheapest reliable decision first.
- 01Do not ask KV what the Host already tells youPlatform-owned Hosts can be recognized directly.
- 02Consolidate records with the same purposeOne read determines dynamic or static delivery.
- 03Check the body cache firstA static HIT skips routing altogether.
- 04Briefly share valid routes and 404sDo not repeat the same decision on every request.
- 05Treat freshness as a budgetDesign acceptable propagation before extending TTLs.
At a multi-tenant entry point, every small read is multiplied across all tenants and requests. Designing where a request can stop—across Workers, KV, the Cache API, and delivery paths—reduced reads without turning the gateway into an unmaintainable system.