Reducing Cloudflare Workers KV Reads for Multi-Tenant PaaS Hosting

We moved from reading KV on every request to reusing cacheable routing decisions. This is how KamuiDash optimized edge routing for a multi-tenant PaaS and cloud hosting platform.

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.

Request path after optimizationREQUEST PATH
01RequestHost + Path
02Static body cacheA HIT skips downstream reads
03Route CacheBriefly shared per Host
04Routing KVRead only on a MISS
STATICStatic deliveryHTML / CSS / Assets
DYNAMICTunnel + AppDynamic bodies are not shared
Cheap decisions come first: static body, resolved route, KV, and finally the delivery target.

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.

Reads before optimizationPER REQUEST
PLATFORM HOSTapp.platform.example
APP KV
At least one read per request
CUSTOM DOMAINcustomer.example
DOMAIN KVAPP KV
Two-stage resolution
Repeated requests for the same Host performed the same KV operations on every Worker invocation.

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.

Host resolution decision treeDOMAIN LOOKUP
Who owns the incoming Host?Decide from the normalized hostname
APEXplatform.exampleRoute to the landing applicationDOMAIN KV: 0
SUBDOMAIN*.platform.exampleUse it as the internal HostDOMAIN KV: 0
CUSTOMcustomer.exampleResolve the internal HostDOMAIN KV: 1
Ownership is known from the Host itself, so platform-owned domains skip the first KV read entirely.

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.

Consolidating routing recordsONE ROUTING RECORD
BEFORE
Dynamic routing KVapp → dynamic delivery data
Static routing KVapp → static delivery data
AFTER
Unified routing KVDelivery type, state, and required data
in one record
The control plane and Worker moved to the new schema together, without leaving a long-lived fallback.

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.

Changing the order of workEARLY RETURN
BEFORE
  1. Resolve Host
  2. Read routing KV
  3. Identify static route
  4. Check body cache
  5. Retrieve static file
AFTER
  1. Check body cache
  2. Resolve Host
  3. Read routing KV
  4. Retrieve static file
A static cache HIT reaches neither KV nor downstream static-delivery work. The Worker runs, but exits near the entry point.

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.

Route Cache MISS and HITSHORT CACHE WINDOW
FIRST REQUEST
WorkerRoute Cache MISSKV ReadCache storeDelivery target
NEXT REQUEST
WorkerRoute Cache HITKV Read 0Delivery target
After a dynamic Route Cache HIT, the request still proceeds to the application because its user-specific body is not shared.

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.

Negative caching for a 404NOT FOUND IS A RESULT
First requestUnknown HostKV confirms it is absentMISS
BrieflyStore the 404 resultStore absence onlyRoute Cache
Following requestsReturn the same 404Do not reach KV0 Reads
Absence is reusable too. Abnormal traffic is controlled by a separate protection layer rather than by cache behavior alone.

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.

Two cache positionsEXECUTION & BILLING
CACHE API
RequestWorker runsCache HIT
  • Worker request is billed
  • Worker CPU executes
  • KV and downstream reads are skipped on HIT
WORKERS CACHING
RequestCache HITWorker skipped
  • Worker request is billed
  • Worker CPU does not execute
  • KV and downstream work are skipped
A Workers Caching HIT still counts as a Worker request, but it avoids Worker CPU time and all downstream processing.

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.

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.

Rollout and verificationSAFE CHANGE
  1. 01Local testsHealthy, 404, in-progress, malformed, and cache errors
  2. 02Development planOnly the intended in-place update
  3. 03Development URLsDynamic, static, and unknown Hosts
  4. 04Production planReconfirm the isolated Worker update
  5. 05Production URLsPlatform, custom, static, and 404 routes
We also compared the deployed Worker body with the reviewed local source before considering the rollout complete.

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.

Summary

The important change was not adding a single cache. It was putting the cheapest reliable decision first.

  1. 01
    Do not ask KV what the Host already tells youPlatform-owned Hosts can be recognized directly.
  2. 02
    Consolidate records with the same purposeOne read determines dynamic or static delivery.
  3. 03
    Check the body cache firstA static HIT skips routing altogether.
  4. 04
    Briefly share valid routes and 404sDo not repeat the same decision on every request.
  5. 05
    Treat 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.

References