Repository navigation
Serverless
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.
-
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 asnotify failedwithout 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/statusread the state back (?format=text,?tag=). - Both the Vercel handler and the Worker also run in the automated tests against fakes.
| 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).
| 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).
| 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. |
- Copy
pulsecheck.example.ymltopulsecheck.ymlin 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. -
vercel(or import the repo in the dashboard). Vercel findsapp.py. - 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 injectsKV_REST_API_URLandKV_REST_API_TOKEN(also accepted:UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN). - Environment variables:
CRON_SECRET(random, 16+ characters; Vercel sends it asAuthorization: Bearer …, and the endpoints refuse everything else) plus every${VAR}in your YAML. - Schedule: edit
vercel.json. Pro:"schedule": "*/5 * * * *". Hobby: keep daily and use.github/workflows/trigger-vercel.yml(GitHub variableVERCEL_CRON_URL=https://<project>.vercel.app/api/cron, secretCRON_SECRET). -
vercel --prod, thencurl -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, usevercel curl /api/cron.
-
npm install --global cfandcf auth login. You also needuv. - Copy
pulsecheck.example.ymltopulsecheck.yml(HTTP sites; alert URLs from the built-in list). - Export every
${VAR}your YAML uses, plusSTATUS_TOKENif you want/statusand/run. -
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. - 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. - State:
cf kv keys get pulsecheck --namespace-id <id> --text. A crashed run leaves its reason underpulsecheck-error. - Endpoints (need
Authorization: Bearer $STATUS_TOKEN):GET /status?format=text|md|json&tag=x,GET /run?force=1&tag=x.
Inside inline { ... } mappings, quote any value containing ${VAR}: {name: api, url: "https://x.example.com/${TOKEN}"}.