DEV Community

Max
Max

Posted on

4 perf walls I hit shipping an AI hub on Cloudflare Workers KV

Status pages don't aggregate. AI news lives in 60 RSS feeds. MCP servers are scattered across awesome-lists.

So I built Prismix - one URL for all three - on Cloudflare Workers + Astro 5. Here's what broke and what fixed it.

Live: prismix.dev


Why this exists

Three daily frustrations:

  1. OpenAI / Anthropic status pages don't aggregate — I had 5 tabs open every morning
  2. AI news lives in 60 different RSS feeds and Twitter accounts
  3. MCP servers are scattered across awesome-lists with no canonical index

Prismix solves all three on one page.


The stack

Layer Choice Why
Framework Astro 5 SSR Islands architecture, $0 hosting
Hosting Cloudflare Pages Free tier covers MVP
Database Cloudflare Workers KV No DB to operate
Auth Self-rolled (email + GitHub) Saved $25/mo vs Clerk
Payments Ko-fi No Stripe overhead
Tests Vitest + MockKV 1112 tests, runs in 8s

The whole thing costs $0 to run. That constraint forced 4 lessons.


Lesson 1 — KV writes are the rare resource

Cloudflare's free tier gives you 100,000 KV reads/day but only 1,000 writes. I hit the wall at 1pm on day 12.

The fix wasn't fewer writes - it was a diff-or-skip pattern: read the existing value, compare, only write if changed.

async function setIfChanged(kv: KVNamespace, key: string, value: string) {
  const existing = await kv.get(key);
  if (existing === value) return false; // skip
  await kv.put(key, value);
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Cron writes dropped from 8,400/day → 600/day. Reads are 100× more generous - trade them freely.


Lesson 2 - JSON.parse has a cliff at 1 MB

The news index grew to 5,000 items. Each KV read returned a 3.2 MB JSON blob. SSR for /news was taking 330ms just to parse.

The fix was splitting the index into a "first page" slice (250 items, 165 KB) and a meta blob (facet counts, 12 KB). When the user hits /news with no filters or pagination, we only read the slice.

// hot path — 17ms
const firstPage = await kv.get('news:index:latest:firstpage:v1', 'json');
const meta = await kv.get('news:index:latest:meta:v1', 'json');

// cold path (filters, sort, page>1) falls back to full index
Enter fullscreen mode Exit fullscreen mode

Parse time: 330ms → 17ms.


Lesson 3 — CF Workers subrequest concurrency is per-request

This one bit me hardest. /mcp was rendering in 5.5s. Server-Timing showed 4.31s spent on KV reads.

I assumed 60 parallel kv.get() calls would run in parallel. They don't — Workers caps concurrent subrequests at ~6, so the rest queue.

The fix: a denormalised snapshot blob updated by cron once a day.

// Before: 60 parallel reads, serialised by concurrency limit → 4.31s
const likes = await Promise.all(slugs.map(s => kv.get(`mcp:likes:${s}`)));

// After: 1 blob read → 30ms
const snapshot = await kv.get('mcp:bulk-engagement:v1', 'json');
Enter fullscreen mode Exit fullscreen mode

Total /mcp SSR time: 5460ms → 380ms.


Lesson 4 — Even fast SSR feels broken without instant feedback

After fixing perf, the site loaded in 200-400ms. Users still complained it was slow.

The problem: the browser doesn't repaint the address bar until the new HTML arrives. For 200ms the user sees nothing — they click again, thinking the button didn't register.

Two fixes:

  1. Pointerdown progress bar — render a 2px green bar at the top of the screen on pointerdown, before the navigation even fires.
  2. Astro tap prefetch — start fetching the next page on pointerdown, finish during the click delay.
document.addEventListener('pointerdown', (e) => {
  const link = e.target.closest('a[href]');
  if (link) showProgressBar();
});
Enter fullscreen mode Exit fullscreen mode

Perceived load time dropped from "slow" to "instant" without a single SSR optimisation.


What didn't work

  • Algolia for MCP search — overkill; switched to in-memory tokenised index
  • Stripe — Ko-fi's webhook + idempotency was 1 day vs Stripe's week
  • Clerk — self-rolled OTP + GitHub OAuth was 3 days, $0/mo
  • GitHub Actions schedule: — drifts by minutes, silently skips. Moved cron to cron-job.org

What's next

  • Per-service alert thresholds (Pro feature)
  • AI news instant-notify via webhook (Discord / Slack)
  • Public read API + SDK

If you've shipped on Cloudflare Workers and hit different walls, I'd love to hear what broke for you. Comments open.

prismix.dev

Top comments (0)