jobcache-mcp hosting — Render vs. Cloudflare

Status: draft recommendation, 2026-05-17. Co-locating MCP with its backing API is the stated direction. This study weighs it against the status quo.

1. Render Hobby free-plan limits

From render.com/docs/free:

Constraint Value Scope
Services per workspace 25 workspace
Free instance-hours 750 / calendar month workspace total, shared across all free services
Spin-down after idle 15 minutes with no inbound traffic per service
Cold start ~1 minute per spin-up
Filesystem Ephemeral (lost on redeploy/spin-down) per service
RAM / CPU per service Not documented for Free tier
Bandwidth / pipeline minutes Free quota included; overage billable workspace total

Two implications dominate everything else:

  • 750 hr/mo = 24 hr/day across the whole workspace. Two always-on free services already overflow (2 × 720 hr = 1440 hr). The current Hobby workspace must be relying on Render's spin-down or paid time on at least one service.
  • A spun-down service costs zero hours, but the next request waits ~60 s.

2. Current JobCache footprint

jobcache/infra/render.ts provisions two free Web Services in frankfurt:

  • jobcache-api (Row 3) — public RADAR query endpoint. Request-driven.
  • jobcache-heavy-worker (Row 4) — cron-triggered scraper.

Both services share the same repo/branch, so a push deploys both. Neither disables auto-deploy. At 750 hr/mo total, the workspace is already over the always-on budget for two services. Either Render is spinning the heavy worker down between runs (likely — it's cron-triggered and idle most of the day) or the workspace has already received the overage warning. Cold-start on the heavy-worker is irrelevant (a cron tick that takes 60 s longer to start is invisible). Cold-start on the api is visible to a first hit but acceptable for a low-traffic backend.

3. Three options

a. Move jobcache-mcp to Render as a third Web Service

  • Capacity: 25-service cap is irrelevant; instance-hours is the bind. Adding a third service that is mostly-idle adds ~0 hours/month so long as the 15-min spin-down lets it sleep — fine for MCP traffic, which is bursty and agent-driven.
  • Cold start: ~60 s on first MCP request after idle. Hostile to an AI agent that just wants tools/list and a quick lookup. Agents typically use a 5-30 s tool timeout; 60 s blows past it.
  • Pulumi: add one more new RenderService("jobcache-mcp", { ... }) block modelled on api with startCommand: "pnpm run start:mcp". The dynamic provider's adoption-by-name path means even a manual pre-create is safe. Zero schema work.
  • Architectural win: MCP and API in one process; the local-dev URL (127.0.0.1:3000) goes away because they're co-deployed.

b. Keep jobcache-mcp on CF Workers (status quo)

  • Always-warm, sub-50 ms cold start, integrates with @cv/perspective-shared (admin-auth) and uses the same Hono toolchain as careervector-mcp.
  • Cost is a few CF requests per MCP call (each tool call is 1 inbound + 1 outbound HTTP to jobcache.corbet.ch). At CV's MCP volumes (tens of calls/day) this is rounding error against the 100k/day free CF tier — even allowing for the cf-quota post-mortem (CLAUDE.md §22).
  • Downside: violates the new "every MCP worker is based on an API" rule by putting the MCP on a different host from its API, but the rule is about coupling, and the worker is already coupled at the URL level (JOBCACHE_SERVICE_URL).

c. Embed MCP routes into jobcache-api (Hono mount)

  • One Render service hosts both. app.route('/mcp', mcpApp) mounts the MCP Hono app on the existing api process — Hono supports this natively (both apps already use Hono). Zero new infra, zero new spin-down, zero cold start beyond api's own.
  • Saves one slot in the 750 hr/mo budget.
  • Couples release cycles: MCP and API ship on the same push. For the current state (3 read-only MCP tools that wrap api endpoints) this is a feature, not a bug — they're semantically locked together.
  • Path or host split: the /mcp path is fine on jobcache.corbet.ch, or wire a mcp.jobcache.corbet.ch CNAME at the Cloudflare layer. The cockpit-vision doc already pencils mcp.jobcache.corbet.ch as a separate host (architectures.md §"API layering"), but the host can resolve to the same upstream service.

4. Recommendation

Option (c) — embed MCP routes into jobcache-api. Reasoning:

  1. Spec compliance: "every MCP worker is based on an API" — same process is maximum coupling. Beats option (a) which is co-host, not co-process.
  2. Cold start solved: API serves both, so MCP cold-start follows whatever the API does. API is the same shape, same risk. No 60-s MCP timeout cliff.
  3. Budget-positive: keeps two free services (api+heavy-worker) instead of three. The 750 hr/mo budget stays uncomfortable but no worse.
  4. Hono pattern: jobcache/mcp/src/index.ts is already a new Hono() app tree. Mount via app.route('/mcp', mcpApp) in jobcache/src/server.ts.
  5. Future-flex: if MCP grows write tools that need the JOBCACHE_CONTROL_TOKEN (currently kept off CF Workers per the tools.ts header note), the token is already in api's env and a co-process gate is straightforward.

CF Workers (option b) is the kill-switch, not the recommendation: keep it as a deployable fallback while the in-process mount is bedded in.

5. Reversal path

If (c) bites:

  • Cold start on api becomes painful for MCP agents: wire mcp.jobcache.corbet.ch to a re-deployed CF Worker (the existing jobcache/mcp/ worker, unchanged). Worker calls jobcache.corbet.ch over HTTP exactly as today. Five-minute reversal.
  • Render workspace overruns 750 hr/mo: split MCP back to CF Workers first (it's the smallest of the three), then evaluate Hobby → Starter on the heavy-worker.
  • MCP write tools land and you don't want them in the same process as api scrape control: move just the MCP back to a third Render service (option a) or a CF Worker (option b).

The jobcache/mcp/ worker code and wrangler.toml stay in the tree as the reversal artifact; do not delete after the embed lands.


6. Status — 2026-05-17

Embed implemented and committed (this same change set):

  • MCP sub-app moved into jobcache/src/mcp/{index,tools,types}.ts. Mounted on the main api Hono app via app.route('/mcp', createMcpApp()) in jobcache/src/server.ts. Tools call the in-process data layer (db.ts, fact-tree.ts, metrics.ts) directly — no inter-service HTTP hop.
  • Standalone jobcache-mcp CF Worker retired: jobcache/mcp/ directory deleted; top-level package.json workspaces entry removed. Reversal artifact lives in git history (revert this commit to restore). Section 5's "do not delete after the embed lands" note is superseded — adoption is irreversible-by-design; the kill-switch is "revert the embed" rather than "redeploy the standalone worker".
  • JSON-RPC serverInfo.name updated from jobcache-mcp to jobcache to reflect that the MCP is now a perspective of the Render-hosted service, not its own product.
  • Subdomain: mcp.jobcache.corbet.ch provisioned via a jobcache-mcp-proxy CF Worker (declared in jobcache/infra/cloudflare.ts) that rewrites //mcp and forwards to the Render-hosted jobcache-api. The proxy worker is structurally a path rewrite, not the MCP server itself — hence the new -proxy suffix to distinguish it from the retired jobcache-mcp worker name. The hostname is bound via cloudflare.WorkersDomain (custom domain) rather than WorkerRoute+AAAA because only WorkersDomain triggers CF to auto-provision the per-hostname SSL cert that mcp.jobcache.corbet.ch needs — the zone Universal SSL cert covers *.corbet.ch (first-level subdomains) but NOT third-level hosts like mcp.jobcache.corbet.ch. Quota footprint is well within the CLAUDE.md §22 budget (MCP traffic is tens of calls/day; CF account-wide cap is 100k/day).
  • Render service config (jobcache/infra/render.ts) updated alongside the embed: rootDir set to repo root (was services/jobcache, broken by the structural migration), build switched to bun install --frozen-lockfile, start commands use bun run --filter jobcache start[:worker] to dispatch into the workspace member. The Render services consume bun.lock at the repo root.
  • Operator picks up DNS by running pulumi up from jobcache/infra after setting the new jobcache:cloudflareAccountId secret. Full deploy steps live in jobcache/infra/README.md.

Deployed 2026-05-17: embed live at https://jobcache.corbet.ch/mcp; proxy live at https://mcp.jobcache.corbet.ch/. Standalone jobcache-mcp CF Worker deleted. Both URLs return identical MCP JSON-RPC payloads (verified via tools/list, initialize, and get_corpus_health tool call).


Sources:

  • Render docs — Free plan
  • jobcache/infra/render.ts, jobcache/infra/render-provider.ts
  • jobcache/src/mcp/{index,tools,types}.ts, jobcache/src/server.ts
  • jobcache/infra/cloudflare.ts
  • wiki/content/studies/cockpit-vision/architectures.md
  • CLAUDE.md §22 (CF quota), §23 (compute backend pattern)
Source: wiki/content/studies/cockpit-vision/jobcache-mcp-hosting-investigation.md