Building a server monitoring SaaS on Cloudflare Workers — architecture, decisions, mistakes A solo developer built Pulse, a server monitoring SaaS for Latin American small businesses, running entirely on Cloudflare Workers, Turso, and R2. The platform costs about $5 per month for the first hundred customers and achieves sub-second alert latency, using a simple cron-based architecture instead of a streaming pipeline. Pulse is a server monitoring SaaS I built as a solo founder for Latin American small businesses. It runs entirely on Cloudflare Workers + Turso libsql + R2, with a Go agent that installs on Linux, macOS, Windows, or Docker via a single curl command. The whole platform costs about $5 per month to operate for the first hundred customers and hits sub-second alert latency at the edge. Stack in one line: Cloudflare Workers for compute, Turso libsql for the multi-tenant database, R2 for agent binary delivery, Workers AI Llama 3.3 for alert interpretation, Resend for magic-link email, Telegram Bot API for alerts, and Stripe for billing. The interesting part isn't the stack itself — it's the tradeoffs. I'll walk through five decisions that shaped how the thing actually works, with code from the real repo. No inventory of features, just the architecture and what I'd change if I started again. Small and medium businesses in Latin America don't fit the pricing model of enterprise-grade observability tools. A local e-commerce with 15 servers cannot justify a monitoring bill that scales unpredictably with logs and metrics ingested. And the free/self-hosted alternatives require operational skills that a two-person infra team doesn't have to spare. The result is that most LATAM SMBs simply don't have real monitoring. Outages get detected when a client calls. That's the market Pulse exists for. Constraints I set for myself: Everything downstream was designed around those constraints. Pulse is one Cloudflare Worker pulse-api that serves five kinds of traffic: /app/ routes for authenticated users /agent/register and /ingest for the Go agent to talk to /silence , /status Plus a scheduled handler that runs every minute: // src/index.js async scheduled event, env, ctx { ctx.waitUntil evaluateAlerts env ; ctx.waitUntil runChecks env ; const scheduledMinute = new Date event.scheduledTime .getUTCMinutes ; if scheduledMinute === 5 { ctx.waitUntil runRetention env ; } } The cron does three things: evaluate thresholds, run HTTP uptime checks, and once an hour at minute 5 run retention cleanup. That's it. There is no separate cron worker, no queue, no background service. The wrangler.toml shows the whole binding surface: name = "pulse-api" main = "src/index.js" compatibility date = "2026-08-01" routes pattern = "pulse.shannonops.com" custom domain = true vars TURSO DATABASE URL = "libsql://pulse-db-shannonops.aws-us-east-2.turso.io" PUBLIC URL = "https://pulse.shannonops.com" r2 buckets binding = "RELEASES" bucket name = "pulse-releases" triggers crons = " " ai binding = "AI" One worker, one database, one bucket, one cron. That's the whole platform. The temptation was to build a streaming pipeline: ingest a sample, evaluate rules in-line, fire an alert if a threshold is breached. That's how enterprise tools do it. I picked the boring alternative: a plain cron trigger every 60 seconds that reads all enabled thresholds, joins them against recent samples, and emits alerts. js // src/alerts.js export async function evaluateAlerts env { const client = turso env ; const now = Math.floor Date.now / 1000 ; const stats = { evaluated: 0, fired: 0, resolved: 0, notified: 0 }; const thRes = await client.execute { sql: SELECT id, tenant id, host id, metric, operator, value, duration min, severity, name, enabled FROM thresholds WHERE enabled = 1 , args: , } ; const hostRes = await client.execute { sql: SELECT id, tenant id, hostname, last seen at, silenced until FROM hosts , args: , } ; const hostsById = new Map hostRes.rows.map r = String r.id , r ; for const th of thRes.rows { const targetHosts = ...hostsById.values .filter h = String h.tenant id === String th.tenant id .filter h = th.host id || String h.id === String th.host id ; for const h of targetHosts { if h.silenced until && Number h.silenced until now continue; stats.evaluated++; const violation = await evaluateThresholdForHost client, th, h, now ; if violation.action === "fire" stats.fired++; if violation.action === "resolve" stats.resolved++; } } await notifyPending client, env ; return stats; } Why the boring version wins here: If I ever needed real sub-second alerts say, for financial systems , I'd add a fast-path: evaluate a small subset of thresholds inline on POST /ingest . But I haven't needed to. curl | sudo bash install, and R2 for binary delivery The agent is a static Go binary compiled with gopsutil for cross-platform metrics collection. Around 6MB, no runtime dependencies. Users install it with: curl -sSL https://pulse.shannonops.com/install.sh | sudo INVITE=xxx bash The install.sh is generated by the Worker itself — no external CDN, no GitHub release page: js // src/install.js excerpt const INSTALL SH = /usr/bin/env bash set -euo pipefail API URL="\${PULSE API:-https://pulse.shannonops.com}" INVITE TOKEN="\${INVITE:-}" OS="\$ uname -s | tr ' :upper: ' ' :lower: ' " ARCH="\$ uname -m " case "\$ARCH" in x86 64|amd64 ARCH="amd64" ;; aarch64|arm64 ARCH="arm64" ;; esac BIN URL="\$API URL/dl/pulse-agent-\$OS-\$ARCH" BIN DEST="/usr/local/bin/pulse-agent" curl -fsSL -o "\$BIN DEST" "\$BIN URL" chmod 755 "\$BIN DEST" "\$BIN DEST" -api "\$API URL" -invite "\$INVITE TOKEN" ; The /dl/pulse-agent-