← Back to Konde Blog
ENGINEERING

Why we built kdoc on Cloudflare Workers

kdoc serves docs and blog content for the entire Konde footprint at sub-50ms globally for less than five dollars a month. Here is the architecture, the trade-offs, and the three things that surprised us about Workers in production.

kdoc is the docs and blog platform you are reading this post on. It serves every Konde-flavoured documentation site (docs.konde.io, blog.konde.io, the per-product docs we host for partners) from a single Worker codebase, with content stored in Workers KV, deployed once per tenant via separate Wrangler envs.

Total monthly bill across two production tenants and a handful of staging envs: under $5. Median page render time, measured globally: 35–45ms. Here is how it is put together and why we chose this stack over the obvious alternatives.

The shape of the workload

kdoc has the easiest workload in computing: serve static-ish HTML, sometimes from cache, sometimes freshly rendered from Markdown. Read-heavy by a factor of about 50,000 to 1. The hot path is "GET a markdown blob from KV, render it through marked, return HTML."

Three constraints matter:

  1. Edge-fast. Konde users live in Indonesia, Vietnam, and the Philippines. A docs site that takes 800ms to load from Jakarta is broken.
  2. Per-tenant isolation. Each Konde-hosted docs site is its own Worker, with its own KV namespace and custom domain. We trade some build complexity for a clean blast radius if any single tenant misbehaves.
  3. No origin server. We do not want a Postgres, a Redis, an S3, or a Node process. The fewer moving parts, the fewer things that page us at 3am.

CF Workers + KV is exactly this stack.

How a page renders

Walk through the request for /en/announcements/2026-04-22-seed-round:

  1. Cloudflare DNS points blog.konde.io to our Worker.
  2. Worker entrypoint parses the path, identifies it as a blog post, looks up the slug.
  3. KV read for post:en:2026-04-22-seed-round — typically 5-15ms at the edge.
  4. Frontmatter parse + Markdown render via gray-matter and marked.
  5. Layout assembly — applies the active theme tokens, header, footer, the post template.
  6. Response — HTML body with Cache-Control: public, max-age=300.

Total time on the wire (Singapore POP, recent runs): 32-48ms.

The second request to the same post hits Cloudflare's edge cache and skips the Worker entirely. Edge cache hit time: 8-12ms.

What KV gets us

Workers KV is eventually consistent, written-by-control-plane, read-by-edge. For a docs platform that publishes a few times a day, this model is ideal. Write latency does not matter — you publish, the world sees it within 60 seconds. Read latency is what users feel, and KV reads at the edge are stupid fast.

The schema is flat:

  • post:{lang}:{slug} — the raw Markdown blob with frontmatter
  • _blog_index:{lang} — pre-computed list of slugs ordered by date, for the listing page
  • _blog_config — the language and category configuration
  • _author:{slug} — author registry (name, role, avatar) referenced by author: in frontmatter

This is the entire data model. No joins, no relations, no migrations. When we need a different view of the data, we add a new pre-computed index keyed differently.

The three surprises

Things we learned shipping Workers in production that the marketing pages do not tell you.

Surprise 1 — there is no setTimeout outside a request

Workers do not have a long-running event loop. There is no daemon you can spin up to do periodic background work. The first time we needed to refresh a cache nightly, we wrote it as a Cron Trigger (a separate Worker invocation that fires on schedule). It works, but it is a different mental model from "your Node process can do whatever it wants whenever it wants." Plan for it.

Surprise 2 — KV reads from a Worker are not free

KV reads are billed per-request once you cross the free tier. For a high-traffic page, the cost adds up faster than CPU time does. We solved this with aggressive Cache-Control headers, so most reads come from edge cache, not KV. The Worker only renders cold pages.

Surprise 3 — bundle size matters more than you think

Workers have a 1MB code-size limit (after compression). gray-matter pulled in js-yaml, which pulled in 200KB of stuff we did not need. We had to switch to a hand-rolled frontmatter parser for a few of our heaviest sites. Watch your dependency tree.

What we did not pick

Vercel + Next.js. Excellent product. The cost model (per-build minutes, per-bandwidth, per-function invocation) makes it expensive to run dozens of small docs sites. For one heavy site, totally reasonable. For our shape of workload, Workers wins.

S3 + CloudFront + a build step. This is the classic "static site generator, deploy on push" model. It works, but the publishing flow is "rebuild the world every time you publish," and we wanted "edit one Markdown blob, change is live in 60 seconds."

A self-hosted Hono + node origin. Cheaper if you only run one site. Adds operational overhead (a process to monitor, a server to keep alive) that compounds across tenants. Not worth it at our scale.

The takeaway

If you have a read-heavy, low-write, multi-tenant publishing workload, CF Workers + KV is hard to beat. If you have anything like a real database, a long-running background task, or a >1MB bundle, you should look elsewhere. Konde Hosting (Q2) targets the anything else case — but that is a different post.