# Cloudflare or a VPS for agent-run production

We moved our road-conditions site's app off Cloudflare Workers to a VPS and kept Cloudflare at the edge. When I'd pick each, and why.

Topic: Failures
Tested: 2026-10-28
Revision: 1
By Roman Vaisman
Page: https://eng.vaisman.me/entries/cloudflare-vs-vps/

## In 30 seconds

- By October 10, 16 of the site's 26 incidents were platform or runtime ones; cron then stopped account-wide and support gave no root cause.
- With 7 days of logs and a 2,000-event cap, we couldn't find the cause ourselves either.
- The app, the API and the scheduler moved to a VPS, built VPS-native. DNS, CDN, WAF, Tunnel and R2 backups stayed on Cloudflare.
- I haven't tested whether AWS or others can match Cloudflare's edge.

## Problem

Our road-conditions site, [MountainSwitch](https://mountainswitch.co), ran entirely on Cloudflare: Workers for the web app, the API, the cron processor and the radar, with D1, KV and R2 for data. An agent runs the site day to day.

It just wasn't reliable. By October 10, 2026, 16 of the site's 26 incidents were platform or runtime problems, not our code. On August 20, KV writes failed with 500s and froze the snapshots. On September 19 we got 214 "too many API requests" errors. On October 5, re-homing the cron with the same code raised storage operations by 60% and CPU by 1.7x. On October 10, the median delay before a cron run started went from 3 s to 264 s, and at 10:39 PT cron stopped across the whole account. The support case went to P1, and we never got a root cause.

We couldn't find one ourselves either: logs were kept 7 days and capped at 2,000 events. That's the black box. When it breaks, we have zero control and can only guess. For a simple site, that's an okay trade. For a complex production workflow that an agent runs and has to debug, it isn't.

## What I tried

| Attempt | What happened |
| --- | --- |
| Everything on Workers, D1, KV and R2 | Most incidents were the platform's, not ours. |
| Re-homing the cron, same code | More storage and CPU for the same work. |
| Cloudflare support | P1, no root cause. |
| Finding the cause ourselves | Not enough logs to find it. |
| A VPS staging copy, built VPS-native | Up on October 10, the day I asked; staging switched over on October 14. |
| Production on the VPS, edge left on Cloudflare | DNS moved on October 16 at 13:26 PT. Workers crons stopped at 14:23 and were quiet by 15:43. |

## What worked

**Ask for the right thing.** On October 10 I asked for stability and less complexity, not for another fix.

**VPS-native, not a port.** One rule for the move: code that only exists to get around a Workers limit stays behind.

**Move the moving parts, keep the edge.** The web app, the API and the scheduler moved to the VPS. D1's data went into SQLite, with Litestream for continuous backup. Cloudflare kept DNS, the CDN, the WAF, the Tunnel and the R2 backups. The old Workers sit idle as a rollback.

**Staging first, then production.** Staging switched first. After the production DNS move, we stopped the Workers crons and watched until they went quiet.

**Expect the move itself to bite.** Removing the staging cron on Cloudflare took 3 h 19 m to take effect. A cutover script assumed 32-character hex IDs; the real ones had 40. One production database migration had never been applied. One gate before the switch couldn't be reproduced, so we paused the switch.

**What we got.** Deploys take about 2 minutes and flip back on their own. A rebuild drill took about 5 minutes. After the move, 20 publications went out with 0 fetch errors, and restart p99 dropped from 15,528 ms to 828 ms.

**What Cloudflare still does well.** The edge: on top of DNS, the CDN and the WAF, it serves stale copies when the origin has trouble and caches at the edge. Their edge protection and stuff like that are definitely better, and this Almanac is a static site Cloudflare serves. That part I'm happy with.

**How I'd choose now.** Static sites, caching, protection and storage: Cloudflare is great. For complex, agent-run production workflows, a VPS is probably the better choice: the logs are yours, and the agent can look at the process itself. By simple I mean static or mostly cached pages. By complex I mean scheduled jobs, external data feeds, stateful processing, and agents deploying many times a day.

## Numbers

The case for moving rests on the incident count and the cron numbers.

| Label | Value | Kind |
| --- | --- | --- |
| Platform or runtime incidents, by 2026-10-10 | 16 of 26 | measured |
| Cron start delay p50, before vs 2026-10-10 | 3 s vs 264 s | measured |
| "Too many API requests" errors on 2026-09-19 | 214 | measured |
| Log retention and event cap we hit | 7 days, 2,000 events | configured |
| Restart p99, before vs after the move | 15,528 ms vs 828 ms | measured |
| Deploy on the VPS, with automatic flip-back | about 2 min | measured |
| Rebuild in a drill | about 5 min | measured |
| Publications after the move, and fetch errors | 20 and 0 | measured |

## When it stops being true

If we can keep logs longer and without the event cap, for example by exporting them to storage we own, or support starts naming root causes, most of the black-box argument goes. A VPS is a box we run ourselves, with its own ways to fail. And I haven't checked whether AWS or others have something we could put in front of a VPS with similar performance. I'll look at that later.

## Agent brief

```agent-brief
Help me decide between Cloudflare Workers and a VPS for a production app, and move it if the VPS wins. "(general)" marks advice not from my own move.

First ask me:
- What the app runs, and whether an agent or a person operates it.
- Its incidents, by cause: platform or runtime vs our code.
- Log retention, any event cap, and what support said about root causes.

Criteria:
- Keep Cloudflare for the edge and simple sites (static or mostly cached pages; general): DNS, CDN, WAF, serve-stale, edge cache, R2 backups.
- Complex means scheduled jobs, external data feeds, stateful processing, agents deploying many times a day (general).
- Try first (general): export logs to storage you own; open a support case with the evidence.
- Move only if platform incidents keep dominating and root causes stay out of reach (general).

Constraints:
- VPS-native: never port a workaround for a platform limit.
- Move the app, API, scheduler and database; keep the edge where it works.
- Keep the old deployment idle as a rollback.
- No cost argument without my real figures.

Steps:
- Data: D1 goes into SQLite, with Litestream for continuous backup. KV: into the same database or a small local store (general).
- Keep Cloudflare in front through a Tunnel, which connects the VPS to the edge without opening inbound ports (general).
- Build a VPS staging copy; switch staging first.
- Before production: check every database migration is applied, test the cutover script on real IDs, rerun every gate. A gate that won't reproduce pauses the switch.
- Move production DNS, stop the old scheduled jobs, watch until they're quiet.
- Deploy with automatic flip-back: keep the previous release and switch back if the health check fails (general). Time a rebuild drill.
- Compare fetch errors, restart p99 and deploy time before and after.

Pitfalls:
- A platform-side schedule change can take hours to apply: ours took over 3.
- The same code in a new home can cost more storage and CPU: measure.
- The VPS is now yours: keep logs long enough to find a cause, patch, monitor and back it up (general).

Open question:
- Can AWS or another provider match Cloudflare's edge protection and performance in front of a VPS? Untested.

Done when:
- The app serves from the VPS with Cloudflare's edge in front.
- The old scheduled jobs are quiet; the old deployment is an idle rollback.
- A failed deploy flips back on its own; the rebuild drill has a time.
- The agent can read enough logs to name the next incident's cause (general).
```

## Related

- [Coding agents on Linux boxes, the Mac for Apple builds](https://eng.vaisman.me/entries/agents-on-linux.md)
