Skip to content

Serverless

Arian Omrani edited this page Oct 11, 2026 · 5 revisions

Serverless: Vercel and Cloudflare Workers

Language: English · فارسی

pulsecheck can run on Vercel (cron job + Python function) and Cloudflare Workers (cron trigger + Python Worker). Both are sandboxes, so some features are missing there. This page lists what works, every limit that matters (from the platforms' documentation and from real deploys), and the setup.

Keep your deployed copy private. The config you deploy lists your hostnames. Deploy from a private repository or a local folder, never from the public template.

What was tested for real (2026-10-11)

  • Cloudflare: deployed with cf deploy, ran from its own cron trigger every 5 minutes, checked sites, wrote state to KV, sent a reminder through a webhook (HTTP 200), and logged to Workers Logs. A failing ntfy.sh alert returned HTTP 429 and was logged as notify failed without stopping the run.
  • Vercel: deployed, the endpoint rejected a missing secret (401), accepted the right one, loaded the config, and a function downloaded and ran the real shoutrrr and sent a message. With a real Upstash Redis attached, two forced runs checked both sites, kept the state between runs, sent the alert through a webhook, and /api/status read the state back (?format=text, ?tag=).
  • Both the Vercel handler and the Worker also run in the automated tests against fakes.

What works where

Feature CLI / CI / cron Vercel Cloudflare Worker
HTTP checks yes yes yes
TLS certificate expiry yes yes no (not visible to fetch)
TCP checks yes yes no (skipped)
ip_version (force IPv4 / IPv6) yes yes, if the function has that connectivity (not tested) no (those sites are skipped)
Ping checks yes no (no ping in the function) no (skipped)
Alerts all shoutrrr services built-in senders for 12 services, plus the real shoutrrr (downloaded on first use) for everything else built-in senders for 12 services only (list)
State SQLite file Upstash Redis (REST) Workers KV
status command GET /api/status GET /status
OpenTelemetry export, messages, tags, reminders, maintenance yes yes yes
Report with p95 yes no (uptime %, average; 90 days) no (same)

Skipped sites are listed in the run log. max_redirects is ignored on Cloudflare (fetch follows redirects with its own limit).

Limits found

Vercel

Limit Value What we do
Cron frequency Hobby: once per day, any time within the hour. Pro/Enterprise: per minute vercel.json ships a daily schedule so any plan can deploy. On Pro use */5 * * * *. On Hobby use the GitHub workflow trigger-vercel.yml (below).
Cron delivery Best effort: runs can be missed or doubled; no retry; redirects not followed Runs are idempotent. Expect rare gaps.
Python Vercel supports 3.12 (default), 3.13, 3.14. Our .python-version (3.15) broke the build (No interpreter found for Python 3.15). The function itself ran on 3.12.14 vercel.json sets build.env.UV_PYTHON=3.14. The code works on 3.12.
Duration / memory / bundle 300 s default (Hobby max 300, Pro max 800); 2 GB (Pro up to 4 GB); 500 MB Python bundle Plenty.
File descriptors 1,024 shared by all concurrent executions 20 checks at a time.
Disk, Docker, ping /tmp is writable; no Docker; no ping binary State goes to Redis; shoutrrr is downloaded to /tmp (SHA-256 checked) only when an alert URL needs it.
Deploy size A 20 MB binary in the deployment made vercel deploy hang for 25+ minutes on a slow link Nothing big is shipped; the binary is downloaded at run time.
Egress IPs Dynamic You cannot allow-list the monitor's IP without Vercel's paid static IPs.
vercel dev Does not run cron jobs Call the endpoint yourself.

Upstash free plan: 500K commands per month, 256 MB. One run uses 2 commands (about 17,000 per month at 5 minutes).

Cloudflare Workers (Python)

Limit Value What we do
Deploy tool The new cf CLI (beta) cannot build Python Workers yet (its Python dev server package is not published). cf deploy --prebuilt accepts hand-made Build Output deploy/cloudflare/build.py writes the Build Output; cf deploy --prebuilt uploads it.
Modules Only the main module of a Python Worker can be imported; extra files and packages were not importable in the real runtime build.py embeds the whole package into one entry.py.
Packages Pyodide (Python 3.13 in WebAssembly). The Pyodide package index was unreachable from some networks No third-party packages in the Worker. The YAML is converted to JSON at build time.
scheduled(controller, env, ctx) env is None at run time (found in the logs) The Worker uses self.env.
Threads, subprocess, sockets Not usable asyncio and fetch only; no TCP, ping or TLS-expiry checks.
CPU Docs: Free 10 ms per cron run, Paid 30 s. A run of 2 sites used ~24 ms CPU and succeeded The account used for the test ran it without error (plan not checked). Plan on the Paid plan ($5/month) for real use.
Startup 0.6 to 1.1 s reported at deploy Runs once per cold start, not per site.
Subrequests Free 50, Paid 10,000 per run Each site, alert, KV call and telemetry call counts. On Free stay under ~40 sites.
Connections 6 simultaneous 6 checks at a time.
Cron triggers Free 5 per account, Paid 250; UTC; changes can take minutes to apply –
KV Free: 1,000 writes/day (one write per run: a 5-minute cron uses 288/day; a 1-minute cron needs Paid) –
workers.dev From the test machine requests reached the Worker (it logged them) but replies never came back, so /status and /run were checked only against fakes Use your own domain or check from another network.

Vercel setup

  1. Copy pulsecheck.example.yml to pulsecheck.yml in a private fork or clone of the whole repository (the minimal template has no Vercel or Cloudflare files), edit it (use ${VAR} for secrets) and commit it.
  2. vercel (or import the repo in the dashboard). Vercel finds app.py.
  3. State store: vercel integration add upstash/upstash-kv. The first time, open the link it prints and accept Upstash's terms; then run the command again. It injects KV_REST_API_URL and KV_REST_API_TOKEN (also accepted: UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN).
  4. Environment variables: CRON_SECRET (random, 16+ characters; Vercel sends it as Authorization: Bearer …, and the endpoints refuse everything else) plus every ${VAR} in your YAML.
  5. Schedule: edit vercel.json. Pro: "schedule": "*/5 * * * *". Hobby: keep daily and use .github/workflows/trigger-vercel.yml (GitHub variable VERCEL_CRON_URL = https://<project>.vercel.app/api/cron, secret CRON_SECRET).
  6. vercel --prod, then curl -H "Authorization: Bearer $CRON_SECRET" https://<project>.vercel.app/api/cron (?force=1, ?tag=x). Status: /api/status?format=md. If Deployment Protection blocks curl, use vercel curl /api/cron.

Cloudflare setup

  1. npm install --global cf and cf auth login. You also need uv.
  2. Copy pulsecheck.example.yml to pulsecheck.yml (HTTP sites; alert URLs from the built-in list).
  3. Export every ${VAR} your YAML uses, plus STATUS_TOKEN if you want /status and /run.
  4. deploy/cloudflare/deploy.sh pulsecheck.yml (options: --cron "*/5 * * * *", --name pulsecheck). The first deploy creates the KV namespace <name>-state; later deploys reuse it. Secrets are uploaded from your environment on every deploy.
  5. Logs: cf o11y telemetry query --query-id q --view events --limit 20 --timeframe-from <ms> --timeframe-to <ms> --parameters-datasets cloudflare-workers --parameters-order-by-value timestamp --parameters-order-by-order desc.
  6. State: cf kv keys get pulsecheck --namespace-id <id> --text. A crashed run leaves its reason under pulsecheck-error.
  7. Endpoints (need Authorization: Bearer $STATUS_TOKEN): GET /status?format=text|md|json&tag=x, GET /run?force=1&tag=x.

YAML tip

Inside inline { ... } mappings, quote any value containing ${VAR}: {name: api, url: "https://x.example.com/${TOKEN}"}.

Clone this wiki locally