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/listand 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 onapiwithstartCommand: "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 ascareervector-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
/mcppath is fine onjobcache.corbet.ch, or wire amcp.jobcache.corbet.chCNAME at the Cloudflare layer. The cockpit-vision doc already pencilsmcp.jobcache.corbet.chas 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:
- 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.
- 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.
- Budget-positive: keeps two free services (api+heavy-worker) instead of three. The 750 hr/mo budget stays uncomfortable but no worse.
- Hono pattern:
jobcache/mcp/src/index.tsis already anew Hono()app tree. Mount viaapp.route('/mcp', mcpApp)injobcache/src/server.ts. - Future-flex: if MCP grows write tools that need the JOBCACHE_CONTROL_TOKEN
(currently kept off CF Workers per the
tools.tsheader 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.chto a re-deployed CF Worker (the existingjobcache/mcp/worker, unchanged). Worker callsjobcache.corbet.chover 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 viaapp.route('/mcp', createMcpApp())injobcache/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-mcpCF Worker retired:jobcache/mcp/directory deleted; top-levelpackage.jsonworkspaces 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.nameupdated fromjobcache-mcptojobcacheto reflect that the MCP is now a perspective of the Render-hosted service, not its own product. - Subdomain:
mcp.jobcache.corbet.chprovisioned via ajobcache-mcp-proxyCF Worker (declared injobcache/infra/cloudflare.ts) that rewrites/→/mcpand forwards to the Render-hostedjobcache-api. The proxy worker is structurally a path rewrite, not the MCP server itself — hence the new-proxysuffix to distinguish it from the retiredjobcache-mcpworker name. The hostname is bound viacloudflare.WorkersDomain(custom domain) rather than WorkerRoute+AAAA because only WorkersDomain triggers CF to auto-provision the per-hostname SSL cert thatmcp.jobcache.corbet.chneeds — the zone Universal SSL cert covers*.corbet.ch(first-level subdomains) but NOT third-level hosts likemcp.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:rootDirset to repo root (wasservices/jobcache, broken by the structural migration), build switched tobun install --frozen-lockfile, start commands usebun run --filter jobcache start[:worker]to dispatch into the workspace member. The Render services consumebun.lockat the repo root. - Operator picks up DNS by running
pulumi upfromjobcache/infraafter setting the newjobcache:cloudflareAccountIdsecret. Full deploy steps live injobcache/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.tsjobcache/src/mcp/{index,tools,types}.ts,jobcache/src/server.tsjobcache/infra/cloudflare.tswiki/content/studies/cockpit-vision/architectures.md- CLAUDE.md §22 (CF quota), §23 (compute backend pattern)